How can I possibly think and write about programming today? How can I possibly allow myself to think about anything else? More toward client-server.

So, yesterday, my attempts and very small steps were not impressive. One time, I squeaked under the 2x20 bar by about one minute. Today I’ll try more carefully to do better.

To review the “rules”, my 2x20 idea is that no less than every 40 minutes, I want to commit code to the repo that runs all the tests and the game. The commit should include, of course, the new code I’m working on, and it should ideally be in actual operation in all the tests and the game. I say “ideally”. Clearly I want to allow myself to try an experiment, and my local rules are that I don’t have to throw experiments away if I don’t want to. Furthermore, I want to leave open the possibility that I might test-drive some new functionality in one or more sessions, and then integrate it fully wherever it needs to go.

I think that today, I’ll shoot for 1x20, commits after no more than 20 minutes programming, and I’ll stay relaxed about the extent to which the code is fully integrated. Why? Well, it’s easier, but that’s not the whole reason by far.

The point of small steps isn’t to jam code into full operation rapidly. It is to keep all our work at HEAD, running all the tests, so that we never have a big integration issue when some long-running branch finally gets integrated. We want to keep new code and old code running, and to bring the new code into operation when it’s ready.

Yesterday, the amount of work needed was simply more than 20 minutes, and while I probably could have done it so as to have it in place, running tests, but not yet ready to be pulled into action. I tried to do more in one bite than I could, and it slowed me down. Today, I’ll try to do better.

Turning

It’s 0823. The only command left to do with our new request dictionary is turning to a new direction. Let’s see how that works now:

class Bot:
    def perform_actions(self, actions, connection):
        cohort = Cohort(self)
        for action in actions:
            match action:
                case 'take':
                    connection.take(cohort, self.id)
                case 'drop':
                    # self.holding is an id
                    connection.drop(cohort, self.id, self.holding)
                case 'step':
                    self._old_location = self.location
                    connection.step(cohort, self.id)
                case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST':
                    connection.set_direction(self, action)
                case _:
                    assert 0, f'no case {action}'

For their convenience, the Bots decide on a direction, and the connection has a single method, set_direction that accepts that string:

class DirectConnection:
    def set_direction(self, client_bot, direction_string):
        self.world.command('turn', client_bot.id, direction_string)
        self.update_client(client_bot)

This will want to be changed into a form like the other methods, such as:

    def drop(self, cohort, client_bot_id, holding_id):
        rq = dict()
        rq['entity'] = client_bot_id
        drop_action = {'verb': 'drop', 'param1': holding_id}
        rq['actions'] = [drop_action,]
        self.world.execute(rq)
        result_dict = self.world.fetch(client_bot_id)
        cohort.update(result_dict)

Meanwhile, the World works like this:

class World:
    def set_direction(self, world_bot, direction_name):
        match direction_name:
            case 'NORTH':
                world_bot.direction = Direction.NORTH
            case 'EAST':
                world_bot.direction = Direction.EAST
            case 'WEST':
                world_bot.direction = Direction.WEST
            case 'SOUTH':
                world_bot.direction = Direction.SOUTH
            case _:
                pass

And the request processing looks like this:

class World:
    def execute(self, request):
        id = request["entity"]
        entity = self.entity_from_id(id)
        actions = request["actions"]
        for action in actions:
            self.execute_action(entity, action)

    def execute_action(self, entity, action):
        verb = action["verb"]
        match verb:
            case 'step':
                self.step(entity)
            case 'take':
                self.take_forward(entity)
            case 'drop':
                holding_id = action["param1"]
                holding = self.entity_from_id(holding_id)
                self.drop_forward(entity, holding)
            case _:
                raise Exception(f'Unknown action {action}')

Now it seems to me that if I were to test-drive the turn code into execute_action, I could commit that. It would be integrated, just unused. Then I could change the DirectConnection to use it, and commit again.

The hard part will be writing the test. And even that should be pretty simple.

It’s 0827. I start with the setup and initial check:

    def test_bot_turns(self):
        world = World(10, 10)
        bot_id = world.add_bot(5, 5)
        bot = world.entity_from_id(bot_id)
        assert bot.direction == Direction.EAST

So far so good. Now the request and we’ll get a failure for an unknown operation.

    def test_bot_turns(self):
        world = World(10, 10)
        bot_id = world.add_bot(5, 5)
        bot = world.entity_from_id(bot_id)
        assert bot.direction == Direction.EAST
        rq = {
            'entity': bot_id,
            'actions': [
                {'verb': 'turn', 'param1': 'NORTH'}
            ]
        }
        world.execute(rq)
        assert bot.direction == Direction.NORTH

The test fails:

E     Exception: Unknown action {'verb': 'turn', 'param1': 'NORTH'}

Implement:

    def execute_action(self, entity, action):
        verb = action["verb"]
        match verb:
            case 'step':
                self.step(entity)
            case 'take':
                self.take_forward(entity)
            case 'drop':
                holding_id = action["param1"]
                holding = self.entity_from_id(holding_id)
                self.drop_forward(entity, holding)
            case 'turn':
                direction = action["param1"]
                self.set_direction(entity, direction)
            case _:
                raise Exception(f'Unknown action {action}')

Green. Commit: World understands turn command. It’s 0833. That’s what I’m talkin’ about!

Now we should be able to change DirectConnection with impunity and also without getting into trouble.

    def set_direction(self, cohort, client_bot_id, direction_string):
        rq = dict()
        rq['entity'] = client_bot_id
        turn_action = {'verb': 'turn', 'param1': direction_string}
        rq['actions'] = [turn_action,]
        self.world.execute(rq)
        result_dict = self.world.fetch(client_bot_id)
        cohort.update(result_dict)

OK, now this was a mistake, and it’s one I’ve made before, doing this. This is a change to the signature of the method and it breaks tests.

Roll that back, there’s a better way.

The plan is to create a new method for turning, leaving the old one in place, and then incrementally switch over to the new one.

Since the word ‘turn’ isn’t taken, I’ll call it turn. 0838.

    def turn(self, cohort, client_bot_id, direction_string):
        rq = dict()
        rq['entity'] = client_bot_id
        turn_action = {'verb': 'turn', 'param1': direction_string}
        rq['actions'] = [turn_action,]
        self.world.execute(rq)
        result_dict = self.world.fetch(client_bot_id)
        cohort.update(result_dict)

Same method, new name. Old method set_direction in place. Now, in Bot, call the new method:

    def perform_actions(self, actions, connection):
        cohort = Cohort(self)
        for action in actions:
            match action:
                case 'take':
                    connection.take(cohort, self.id)
                case 'drop':
                    # self.holding is an id
                    connection.drop(cohort, self.id, self.holding)
                case 'step':
                    self._old_location = self.location
                    connection.step(cohort, self.id)
                case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST':
                    connection.turn(cohort, self.id, action)
                case _:
                    assert 0, f'no case {action}'

Green. Commit: using DirectConnection.turn method in Bot.

Now we can search peacefully for users of the old set_direction, which will be a couple of tests.

I find no users. OK, remove the method. Commit that. Makes me worry a bit about turning tests, but we have one now. I think I’d like at least one that runs through Bot. Shall we write one? No, I find two tests about changing direction:

    def test_change_direction_if_stuck(self):
        world = World(10, 10)
        connection = DirectConnection(world)
        client_bot = connection.add_bot(9, 5)
        client_bot.direction_change_chance = 0.0
        client_bot.do_something(connection)
        assert client_bot.location == Location(10, 5)
        assert client_bot.direction == Direction.EAST
        client_bot.do_something(connection)
        assert client_bot.location == Location(10, 5)
        client_bot.do_something(connection)
        assert client_bot.location != Location(10, 5)
        world_bot = world.map.at_id(client_bot.id)
        assert world_bot.direction != Direction.EAST
        assert client_bot.direction != Direction.EAST

    def test_requests_direction_change_if_stuck(self):
        bot = Bot(10, 10)
        bot._old_location = Location(10, 10)
        actions = bot.update_for_state_machine()
        assert actions[0] in ["NORTH", "SOUTH", "WEST"]

That should suffice. I think we’ll sum up here, and then I’ll start a new article, because I’m not done yet.

Summary

We now have all the commands that World understands built into the new request structure, and we have that structure in use. I do think there might be some code using the older ways but I am honestly not sure. We’ll look, probably next time. But the real code is running on the new request.

It remains to create larger, multi-action requests, and then batches of requests. That’ll be coming right up.

Picking a better order for doing things allowed me to commit more frequently with code that isn’t much different from yesterday’s. Of course, today we had some advantages. We had done something much like this yesterday, so we had memory and code to look at, and this was a simpler change to test and to implement, as it didn’t rely on a second entity.

But what really made it easier was changing the order in which I did things, such that nothing broke when I did it. And I get special congratulations for recognizing that I was taking a difficult step and rolling back immediately. It might make sense to do that more often.

Three commits, with intervals of four minutes, nine minutes and three minutes between them. Well done, me.

See you next time!