The Robot World Repo on GitHub
The Forth Repo on GitHub

We have a little game loop running, and it gets keyboard lines and echos them. Should we integrate something into the loop? If so, what? Also: Love one another.

Note

I’ve removed my #StopTheCoup or similar slogans from my heading. Not because it’s not important, but because it reminds me of what perilous times we are in, and I frankly do not need that reminder. I need things, like these articles, to get some of the dark thoughts out of my head. As a replacement for those useless slogans, I’ll see about ginning up a more significant article once in a while. Forgive me, please. I’ve replaced them with something a bit more positive: Love one another.

I did a tiny bit of work yesterday afternoon, cleaning up the new game loop a little, mostly just arranging some constants and using them, and I changed the window a bit, giving it a large space on the left to draw the world and leaving 1/3 of the width for the Forth console. Nothing to see here, but as we work you’ll see some of the changes.

There are two independent somewhat large tasks before us. We need to integrate Forth into the small pane, and to integrate the World into the big pane. Neither of these will be terribly difficult, although I do anticipate not liking the World code as much as I might, because my last commit on that project was December 11th, and that’s long enough to ensure that whatever the code looks like, its flaws will pop right out at me. That’ll be good, because one of the major points of my work here is that we can always improve our code, as substantially as we may require.

I think that I’d like to integrate Forth first. Let’s glance at the code that handles typing a line into the text pane. Expect it to need refactoring: it’s still in experimental spike form.

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode(self.WINDOW_SIZE)
        pygame.display.set_caption('ForthBots')
        self.lines = [
                'Forth> _',
                '4 ok',
                ...
                ]

    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[0] = self.lines[0][:-1]
                        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 + '_'

Quite a slalom course there, always a sign that the code probably needs improvement. Let me think about what’s going on here.

We seem to be editing lines[0]. It starts out containing ‘Forth> _’, intended to represent a Forth prompt with an underbar character indicating where the next character goes. We do not have the ability to show a text cursor, as this is not really a standard text pane. If there is a way to have on in PyGame, I do not know the way.

When a character comes in, we append its unicode to the line, in front of the underbar. When a return comes in we snip the underbar off the current line, and push a new prompt into the list. The list grows without bound, which is perhaps not ideal. When a backspace comes in, we edit the line appropriately.

Now what should happen on return? All that, plus we should extract the part of the lime that represents the Forth statement, submit it to Forth, and push the result onto the list, followed by the prompt.

In for a penny, why not just try it? My first attempt doesn’t quite cut it:

    if event.key == pygame.K_RETURN:
        self.lines[0] = self.lines[0][:-1]
        forth_line = self.lines[0][6:]
        result = self.forth.compile(forth_line)
        self.lines.insert(0, result)
        self.lines.insert(0, 'Forth> _')

I get an ok, but not the result, which is being sent to sysout directly. The ‘.’ word just does print. I do a quick search and find out how to capture sysout to a string. I confess that the Google “AI” answer was what I used. It’s actually useful fairly often. Still a tool of the oppressor though.

Let me refactor a bit before I show the code, get rid of the slalom for readability.

    def main_loop(self):
        self.running = True
        while self.running:
            self.process_events()
            self.screen.fill("midnightblue")
            ...

    def process_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                self.process_keystroke(event)

    def process_keystroke(self, event):
        if event.key == pygame.K_RETURN:
            self.append_forth_result()
            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 + '_'

    def append_forth_result(self):
        self.lines[0] = self.lines[0][:-1]
        forth_line = self.lines[0][6:]
        result = self.call_forth(forth_line)
        self.lines.insert(0, result)

    def call_forth(self, line):
        original_stdout = sys.stdout
        captured_output = StringIO()
        sys.stdout = captured_output
        try:
            result = self.forth.compile(line)
        finally:
            sys.stdout = original_stdout
        return captured_output.getvalue() + result

That actually works rather nicely:

big blue window with two panes, some forth prompts and correct output in the right pane

We’ll commit: Right pane of Game now calls Forth to get results.

This seems to me to be a nice milestone. Let’s look around, reflect, and summarize.

Summary

We have a reasonable-looking window into which you can type Forth statements. It supports backspace, but no other editing.

The implementation has some decent bits: I think the capture of standard out and handling the events in a separate method are decent. But I’m not happy with the slicing and dicing of the lines, adding and removing the underbar cursor and stripping out the Forth prompt, especially with the magic numbers involved, notably the 6, which is the number of characters in ‘Forth>’. We should break out the prompt, and very likely should treat the bottom line as a standalone member somehow. Possibly we should build a small simple object that provides an editable stack of lines and that hides the slicing inside itself.

It might be that there will be two major objects, one managing the Forth pane and one managing the World pane. Certainly the Game class as it stands is managing both and even a rudimentary sense of cohesion suggests that a class that does two different things needs to be or use two classes, one for each thing.

I like to wait until the differences in the larger class are visible enough to give me some certainty about what should be broken out. That prevents me from creating abstractions before their time, but it risks leaving some messy classes lying about. I predict that when we start integrating the World code we’ll see some cases where I’ll wish I hadn’t waited quite so long.

It’s probably best to be a bit more aggressive than I am in breaking things out. Not too soon, before the need is clear, but not holding off too long because we are on a roll and people are breathing down our necks for the next feature. I let things slide too long for two reasons:

First, according to me, because I want to let the code get a bit extra crufty, so that we can experience that it’s always possible to improve it incrementally, even if we have to do the improvements with new features coming along as well.

Second, don’t tell anyone, I let things slide for too long because I get excited about what’s happening and just don’t pay enough attention to what the code needs in order to be, if not ideal, then at least decently good.

Third—I was mistaken about “two reasons”—because I am imperfect and I just don’t see things right away.

Anyway, a fine morning’s work, a visible milestone, and the code is both more capable and better formed than we found it.

See you next time. And do, please, resist the growing idiocy that is our present situation. Write your congress people, vote, contribute to good causes, and love one another.