Design Thinking
The Robot World Repo on GitHub
The Forth Repo on GitHub
Our little typing spike leads me to some design thinking. Spikes often unblock the thought paths, and that happens today. Also a few lines of code and a much improved ‘UI’. #StopTheCoup!
One value to a spike—the quick experiment to try an idea that may help solve some blocking issue—is that it gives us insight into whatever particular problem we were experimenting with. But there’s another common effect that may be even more valuable: a spike often seems to generate ideas and understanding beyond the scope of the actual experiment. I’m not sure why that happens. Maybe the reduction of concern about the problem spiked leaves us more relaxed and able to think more clearly. Maybe having a new fixed point of knowledge to stand on gives us a different view of the situation. Maybe the spike jostles ideas around and new ones come to the top.
Whatever it is, I often find new ideas arising after I’ve completed a spike, and this time is no exception.
Yesterday’s spike set up a tiny PyGame loop that fields keystrokes, and echoes them into the PyGame window. With a little care about what text we display, we can make that window look like the Terminal-style interface that a Forth programmer might expect, little more than a scrolling window of command prompt and input lines.
We have at least a couple of days’ work to turn the spike into something usable, but the central issue seems to me to be resolved, turned from “I have no idea how to do this”, into “let’s see if we can do this better”. That’s the fundamental result we want from a spike. But there’s more.
I haven’t worked on the Robot game since mid-December, and even then, I wasn’t doing anything with the PyGame aspect. So yesterday’s spike kind of reloaded my memory about PyGame and its model of things, and that gives us some things to think about.
PyGame runs a loop that is the control system for the entire game you implement with it. Every so often, typically 60 times a second, PyGame asks the game part of your program to take any actions that the game wants to take, and then the PyGame loop draws the resulting picture. Thanks to the magic of the human brain, that looks like game entities moving around on the screen, and Voila! suddenly you have Asteroids or Space Invaders, or small letters representing bots and blocks appearing to move around in some meaningful fashion.
The key fact here is that the game’s main loop asks the game code to make an incremental change to the world, and does so very frequently, over and over. And that’s not really quite how one might think about it.
Let’s divert ourselves and think about how a robot world might “really work”. The world would have entities in it, robots, blocks, werewolves, ducks, whatever, and those entities would each be fundamentally independent, making their own decisions about the next thing to do and doing it in their own time. The observer, you and I, would see these things moving about, doing duck things. We could imagine that the game loop would have access to the current world state, which would be ever-changing, and the game loop would just display whatever it saw. There would be no snapshot. When we look out in the back yard, we don’t see a set of snapshots of the birds coming to the feeder, we see birds flying about and pecking away.
Now, generally when we simulate a world, we do not have the ability to give each entity its own processor, so we might give it its own data, and its own or shared code, and we would iterate through the entities “very rapidly”, giving each one a chance to do a bit of action. We slice up the apparently continuous world we’re simulating into a series of very small moves, which we do very quickly.
In the case of our Forth, I had been thinking about how one might program a real robot in Forth. The Forth program would probably include a loop that went through the sensing and deciding and powering the motors and what all, very rapidly. The Forth code would include the control loop. So I was imagining Forth updating some common data area and PyGame displaying it, the two of them somehow running independently.
The spike reminded me that it probably can’t work that way. In principle it might be that we could run threads, or multiple processes with some kind of push from Forth to the display code. But we can’t readily do that in Python, because threading in Python is rather cooperative and relies on the ability to wait on some I/O operation, relinquishing the processor to another thread. In our situation both the Forth code and the PyGame code are kind of running full speed.
- Or are they?
- The PyGame code only runs every 60th or a second, and could run as slow as 40 or fewer frames per second without bothersome flicker on the screen. Is there some way we could set up two threads and let Forth run while PyGame doesn’t? And if we did, is that really any different from what we’ll do anyway?
What would we do anyway you ask? Well, the Forth code would surely look like a loop at the top, processing all the entities one at a time:
BEGIN DO-EVERYTHING LOOP
So, in the PyGame situation, we remove that loop, and have the PyGame loop call DO-EVERYTHING
. Presumably, part of DO-EVERYTHING
includes updating whatever structures PyGame looks at to draw the picture.
So, you say, same-same, no real difference. And you’re right, except for the focus of our attention. So far.
But it gets a bit worse. In writing yesterday’s little PyGame loop, I looked at existing code to see how to do it and I noticed this in Robot World:
def on_execute(self):
...
while self._running:
pygame.time.delay(100)
for event in pygame.event.get():
self.on_event(event)
self.clear_screen()
self.draw_grid()
for _ in range(20):
self.run_one_bot_cycle()
self.draw_world()
pygame.display.update()
self.on_cleanup()
def run_one_bot_cycle(self):
message = cohort.create_message()
connection.run_request(cohort, message)
As things stand, what happens at this point is that cohort.create_message
exercises the client-side bots, and each one makes some decisions and creates some requests to World to turn, move, pick up, and so on. Those are converted to dictionaries by the cohort, and the combined message, a list of these dictionary-commands is returned. Then the connection passes that message list of dictionaries to the World, which processes them and turns them back into commands which it does here:
class World:
def execute_verb(self, verb, bot, details):
match verb:
case 'add_bot':
self.add_bot_using(**details)
case 'step':
self.step(bot)
case 'drop':
self.drop_using(bot, **details)
case 'take':
self.take_forward(bot)
case 'turn':
self.turn_using(bot, **details)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn(bot, direction)
case _:
self._add_keyed_message('UNKNOWN_ACTION',verb=verb, details=details)
That method drives the “real” world operations, carrying out operations on the subject bot, such as:
class World:
def step(self, bot):
self.map.attempt_move(bot.key, bot.forward_location()) # changes world version
self.set_bot_vision(bot)
self.set_bot_scent(bot)
You could observe that there should probably be at least two objects here, one that decodes the messages and calls methods like step
, some kind of message decoder, and one that implements those elementary operations, that is, the actual “world”. That observation is likely correct, but we have what we have.
Out of the Weeds
Let’s look up out of the weeds a bit. I think that our Forth code will want to call the methods referred to in execute_verb
above, and that we’ll have no use for the text messages, the connection logic and so on.
In short, the thinking unblocked by our little spike makes me think that we won’t just include and import huge tracts of code from the RobotWorld game. Instead, we’ll probably want to copy the source over and rework it, mostly removing things like the message handling.
How Might We Proceed?
The graphical part of the PyGame code we have uses the world-side map object to find things to draw, because everything in the world is in the map, or in the clutches of something that is in the map. So we might proceed very roughly like this:
- Build up a two-pane window, improving the text pane a bit along the way;
- Populate the two-pane window’s world view side with a few hand-crafted widgets;
- Bring in the map, populate it a bit manually and draw that;
- Possibly give the Forth side the ability to write to the map directly;
- Bring in the world and other supporting objects and wire it in.
In aid of that, I’ll probably draw a couple of rough pictures on a card on the iPad, to get a sense of the dimensions and offsets, and then draw a couple of rectangles on the screen. Or, I might just do this:
class Game:
WHITE = (232, 232, 232)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
BASE_FONT = 'Courier New.ttf'
SIZE = (800, 600)
MARGIN = SIZE[0]/2
...
def main_loop(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
self.lines.insert(0, 'Forth> _')
elif event.key == pygame.K_BACKSPACE:
self.lines[0] = self.lines[0][:-2] + '_'
else:
self.lines[0] = self.lines[0][:-1] + event.unicode + '_'
self.screen.fill("midnightblue")
pygame.draw.line(self.screen, self.WHITE,
(self.MARGIN, 0),
(self.MARGIN,self.SIZE[1]), 1)
rect = self.screen.get_rect()
x = self.MARGIN + 10
y = rect.bottomleft[1] - 24
for line in self.lines:
self.text((x,y),line, 16, self.WHITE, "midnightblue")
y -= 20
pygame.display.flip()
And that might result in this:
Another amazing success! Commit: window appears to have two panes with Forth on right.
OK, we’ll call that product improvement. And it does tell us how easy it can be to get things to look like we want them to look. And, of course, this new inline code has plenty of room for improvement. And we’ll improve it as we go, as we always do.
See you next time! #StopTheCoup!