The Robot World Repo on GitHub
The Forth Repo on GitHub

OK, I’ll do the PyGame text pane. It seems to be the simplest thing that could possibly work Unfortunately, it’s more than one small step. #StopTheCoup!

We’re not going to break anything—at least I don’t plan to—but we’re going to have to build out a bit before we can stand on it. Here’s the plan:

Our “game” will have at least two panes in its window, one for the bots to wander around in, and one for Forth’s prompt and messages. Our next few “milestones” will involve creating a PyGame window and loop, working out a simple scrolling text pane, and then using PyGame’s keyboard events to funnel characters to the pane, and to Forth. We’ll almost certainly need a new Provider, call it PyGameProvider for now, although I think the Provider itself will not know about PyGame.

My starting notion is just to build a separate main into the existing Forth system. It will, I think, replace the existing one in due time.

I don’t love this idea. It feels somehow off at an angle from what I really wanted next, but I have convinced myself that trying to mate a non-blocking Terminal and PyGame is just not on. Doing the text pane now isn’t bad, because using it probably makes for a better “product”. I just wanted to defer it a bit longer.

Let’s get to it.

# game_main.py
if __name__ == '__main__':
    game = Game()
    game.main_loop()

I’ve always built little classes for PyGame, so I’ll do so here. But I don’t have a Game class, so that’s the next step. After a bit of review of old PyGame code, and after discovering that the PyGame site is down, I get this small program to display an empty blue window:

import pygame

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption('ForthBots')

    def main_loop(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
            self.screen.fill("midnightblue")
            pygame.display.flip()

if __name__ == '__main__':
    game = Game()
    game.main_loop()

Ship it! Commit: minimal-ish PyGame foothold.

I am now on hold for a customer support agent regarding an unrelated topic, so if I seem distracted, that’s what’s going on.

I think the next thing is to display some text. I surely have some code somewhere to rip offs> adapt.

Sure enough, I find some code that enables me to write this:

    def main_loop(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
            self.screen.fill("midnightblue")
            self.text((100,100), "Hello, World!", 16, self.WHITE, self.BLACK)
            pygame.display.flip()

    def text(self, location, phrase, size, front_color, back_color):
        font = pygame.font.Font(self.BASE_FONT, size)
        font.set_bold(True)
        text = font.render(phrase, True, front_color, back_color)
        text_rect = text.get_rect()
        text_rect.topleft = location
        self.screen.blit(text, text_rect)

And I get this:

Blue window displaying 'Hello, World' in white on a black rectangle

Ship it! Commit: hello world.

Reflection

OK, that’s not too bad, just a few minutes of thrashing about trying to find simple examples of how I did this once upon a time. And odd things like needing to provide a font file.

OK, what I’d like to do next, I think, is to begin to build up a scrolling window. I figure we’ll have a list of the lines that have displayed, and we’ll draw the most recent at the bottom of the screen, sort of the obvious scrolling window thing.

So my next move will be to create a list of lines and display them. With luck.

    def main_loop(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
            self.screen.fill("midnightblue")
            lines = [
                '4 ok',
                'Forth> 2 2 +',
                'alpha', 'bravo', 'charlie', 'delta', 'echo',
                'foxtrot', 'golf', 'hotel', 'india', 'juliett',
                'kilo', 'lima', 'mike', 'november',
                'oscar', 'papa', 'quebec', 'romeo', 'sierra',
                'tango', 'uniform' 'victor', 'whiskey', 'xray',
                'yankee', 'zulu'
            ]
            rect = self.screen.get_rect()
            y = rect.bottomleft[1] - 20
            for line in lines:
                self.text((10,y),line, 16, self.WHITE, "midnightblue")
                y -= 20
            pygame.display.flip()

blue window showing about 28 lines of text

I see that I missed a comma between ‘uniform’ and ‘victor’. Fixed. Commit: displaying a list of lines.

Reflection

We have a spike that tells us quite a bit about what we need. Here are some of the thoughts that this little experiment gives me:

  • We have some magic numbers in here, the 20 being particularly notable as the spacing between lines (and the offset for the first line). I suspect the “real” value is “something a bit larger than the font size”. Frankly, my dear, I don’t care. Those things always get fiddled until someone likes the display. larger

  • We’ll need to be accepting PyGame key events and buffering them into the list, I guess. Add a new character to the top of the list when it arrives? New line upon receiving one. Our PyGameProvider will presumably prompt, and can emit lines to the list when it prints.

Let’s try a bit of a keystroke thing and then wrap for the morning. With just a bit of hammering, I have this:

    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")

            rect = self.screen.get_rect()
            y = rect.bottomleft[1] - 24
            for line in self.lines:
                self.text((10,y),line, 16, self.WHITE, "midnightblue")
                y -= 20
            pygame.display.flip()

This displays the last line as ‘Forth >_’, and when you type the letter you type appears and the underbar moves. It almost looks like a terminal window. If I could make the underbar flash it would be perfect.

blue window with text lines and last line showing a Forth prompt with some text that has been typed

Commit: rudimentary prompt with backspace.

Let’s sum up: I’m ready for an iced chai.

Summary

In a couple of hours, including many distractions, we have a little spike that provides info on how we might build a scrolling text pane, with a list of strings to be displayed, writing the strings up the screen from bottom (high Y addresses, curse you PyGame) to the top (low Y addresses #$%$#!), with a very rudimentary ability to show typed characters and even to backspace and replace mistakes.

Surely we won’t do it quite this way. I think we’ll want the current line to be separate, if only to avoid all that subscripting. We’ll want arrow keys to move around in the line, although I’m not sure quite how we can represent that on a PyGame screen, and we’ll want up and down arrow to fetch historical lines back for editing. All that is frills: we can do what we really need to do with backspace.

We’ll want all of this to take place in a separate pane of some kind, so we’ll need some lines around the words, and we’ll probably want to be able to adjust the type size and so on and so on and scooby-dooby-dooby. Most of this will not be rocket science, and aside from a line on the screen, hardly necessary.

For me, this is exactly what a spike needs to do: it eliminates ignorance and fear, clears a path to the future, and opens doors to new ideas as we see things taking shape.

I think we can almost start working on our PyGameProvider, although I can imagine putting that off for another session or two as my thoughts clarify about what objects we need and how they should interact.

A very nice start, and most of my confusion of yesterday is now removed. It’s going to be two or three more sessions before we’re ready to wire in the bots, but that’s OK. I’m sure we’d have wanted the text in the window with the bots eventually: I had just hoped to put that off a bit longer.

A good session. I hope to see you next time! #StopTheCoup!