The Repo on GitHub

In client-server mode, I believe that the program will want to accept a batch of commands from a client, process them all, and return a batch result. We’ll discuss briefly why, and then see about doing that.

Why, you ask? Because, at a random guess, a back-and-forth socket connection will take perhaps 100 milliseconds, and if a client was running, say, 20 bots, that would mean it would take two seconds to update the whole batch if we go one at a time. So I conclude, we’d best arrange to deal with a batch. And, should I be wrong, we can always use a batch size of one if we need to.

Design Thinking

Ideas and questions as I sit down at the computer include:

  1. We’ll focus on the server side today, just because I was thinking about it, but also because its requirements may drive some aspects of how the client side code should deal with it.

  2. The batch should include both bot creation requests and bot operations requests. Probably a given client will only do one or the other at any given time, but I can think of no reason offhand to require separate batches.

  3. The batch will probably include a slot for the client’s overall identifier, even though we have not defined that notion yet. Presumably there will be a log-in phase where the client is given an identifier to use thereafter.

  4. Each bot we want to control will have its own little packet, consisting of something like bot id and a command list.

  5. We have surely not defined all the possible commands as yet.

  6. The response batch will provide a response for each element of the input batch. For a bot operation, something like the bot identifier and the associated dictionary that we already provide. For bot creation, we’ll need to define the protocol, but something as simple as the client providing a tentative bot id and the server returning that as a key and the initial bot dictionary will surely suffice.

  7. All this will be formatted in some string-like way, presumably JSON, to be determined.

  8. We will surely want some tests for this and it would be pleasant if those were easy to write and interpret, and it would be ideal if we could replace or improve some of our existing story-like tests.

Note
To the best of my recollection, I have never programmed a socket connection. I have done http connections of various kinds. I am rather certain that a socket connection allows passing string-like information back and forth: what else could it possibly do? So, despite the fact that someday soon I’m going to have to either learn sockets or make friends with someone who understand them, I am confident that this design thinking is good enough. If it’s not right on, it’ll be close enough to be readily adjusted to fit the socket’s needs.

I am focused on my own object, and no matter how much I knew about the hopes and fears and desires of sockets, I would remain focused on what my own objects, the World on one side and the client-side Direct Connection, and Bot, and at least one other object yet to be invented (we’ll see it soon I think). Whatever the preferences of socket code may be, I do not want them contaminating the preferences of my own code. I’ll try to create these batch things in a form that my own objects prefer.

Let’s get started

I think my first intermediate goal will be to provide, in or near World, a way to process a batch of bot operations. We’ll save creation for later, expecting that it won’t be all that different.

As things stand now, I believe that Direct Connection actually sends a single Bot command at a time. Let’s review how things work, starting at Direct Connection.

class DirectConnection:
    def __init__(self, world):
        self.world = world

    def add_bot(self, x, y, direction=Direction.EAST):
        bot_id = self.world.add_bot(x, y, direction)
        result_dict = self.world.fetch(bot_id)
        bot = Bot(x, y)
        bot._knowledge.update(result_dict)
        return bot

    def set_direction(self, bot, direction_string):
        self.world.command('turn', bot.id, direction_string)
        self.update_client(bot)

    def update_client(self, bot):
        result_dict = self.world.fetch(bot.id)
        bot._knowledge.update(result_dict)

    def step(self, bot):
        self.world.command('step', bot.id)
        self.update_client(bot)

    def take(self, client_bot):
        self.world.command('take', client_bot.id)
        self.update_client(client_bot)

    def drop(self, client_bot, block_id):
        self.world.command('drop', client_bot.id, block_id)
        self.update_client(client_bot)

Ah, it’s not quite as I thought. Clearly we call DC multiple times to get things done. That must be coming out of Bot, probably near do_something:

class Bot:
    def do_something(self, connection):
        actions = []
        actions += self.update_for_state_machine()
        self.state = self.state.update(self._knowledge)
        actions += self.state.action(self._knowledge)
        if random.random() < self.direction_change_chance:
            actions += self.change_direction()
        actions += ['step']
        self.perform_actions(actions, connection)

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

Hm. So, in the future, hopefully later this morning, there will be some sort of bot command list identifying the bot and the multiple operations it wants to carry out. As things stand, we’re calling the DirectConnection with one of those operations at a time, and, in the case of the drop, the identifier we’re holding.

Let’s imagine, at least for now, that there will be a new entry point in DC that accepts a command structure and executes it in a batch-like fashion. And we’ll assume that when it’s done, it’ll do the fetch operation that is simulating the return from the socket message. So let’s see … how about we try to TDD-up a World operation that processes a batch kind of thing.

Do we need to define a Batch object? It seems prudent. It’s tempting to think in terms of a list of dictionaries or something, but let’s try to abstract that a bit.

class TestWorldBatch:
    def test_hookup(self):
        assert False

Test fails. Excellent. How will this thing work? Maybe like this:

class TestWorldBatch:
    def test_empty_batch(self):
        world = World(10, 10)
        batch_in = WorldInput()
        batch_out = world.process(batch_in)
        assert batch_out.is_empty()

PyCharm made some excellent suggestions along the way there. Let’s assert what the batch_out is, just for clarity:

class TestWorldBatch:
    def test_empty_batch(self):
        world = World(10, 10)
        batch_in = WorldInput()
        batch_out = world.process(batch_in)
        assert isinstance(batch_out, WorldOutput)
        assert batch_out.is_empty()

We need a couple of classes and a method. PyCharm will help.

class WorldInput:
    pass

class WorldOutput:
    def is_empty(self):
        return True

class World:
    def process(self, world_input):
        from test_world_batch import WorldOutput
        return WorldOutput()

Our test is green. We had to do the import inside the method to avoid a recursion. That issue will go away as soon as we pull the new classes out of the test file, but I have developed the habit of keeping my new classes in their TDD files initially.

Now what? Well, I think the WorldInput is a batch, so it will be some kind of collection of requests. A request has an identifier of the object doing (requesting) the operations, and a series of operations to do, some of them including parameters, presumably other identifiers but defined at best loosely for now.

So let’s imagine what the World would like to do, something like this:

    def process(self, world_input):
        from test_world_batch import WorldOutput
        output = WorldOutput()
        for request in world_input:
            requestor_id = request.identifier
            for operation in request.operations:
                self.command(operation.action, requestor_id, operation.paraneter)
            output.append(self.fetch(requestor_id))
        return WorldOutput()

The command method and fetch method are already part of World, so I’m imagining that our batch will behave as shown there. We get some not unexpected errors:

>       for request in world_input:
E       TypeError: 'WorldInput' object is not iterable

We can manage that trivially, I think:

class WorldInput:
    pass

    def __iter__(self):
        return iter([])

Yes. Our test passes now, although of course we’ll break it right now.

We could commit this code. Am I feeling good enough about it to do so, or are we really just spiking to see what we need to do. We’ll commit. If it’s wrong, we’ll change it. It’s what we do. Commit: initial test and classes WorldInput and WorldOutput.

Let’s test … a step.

    def test_step(self):
        world = World(10, 10)
        bot_id = world.add_bot(5, 5, direction=Direction.EAST)
        operation = WorldOperation(bot_id)
        operation.add_action('step')
        batch_in = WorldInput()
        batch_in.add_operation(operation)
        batch_out = world.process(batch_in)
        assert isinstance(batch_out, WorldOutput)
        assert False

I got this far and couldn’t decide what the final assert should be. Note that we have yet another class here, a WorldOperation. It seems to be based on a bot id, and to contain one or more operations. Kind of makes sense.

Let’s code:

class WorldOperation:
    def __init__(self, bot_id):
        self.bot_id = bot_id
        self.actions = []

    def add_action(self, action):
        self.actions.append(action)

Test is failing. No surprise:

>       batch_in.add_operation(operation)
E       AttributeError: 'WorldInput' object has no attribute 'add_operation'

We enhance that object:

class WorldInput:
    def __init__(self):
        self.operations = []

    def __iter__(self):
        return iter(self.operations)

    def add_operation(self, operation):
        self.operations.append(operation)
Note
I am aware that I’m probably in trouble. I think it’s fair to try, when one feels close, to make the existing test and code pass and then, if we succeed, clean things up. Would I save time by starting over the moment I feel in trouble? I don’t know. Should I try rolling back more readily and see what happens? Maybe I’ll do that in upcoming days.

I’ve taken too big a step. I’m teetering on the edge of the chasm I tried to step across. The process method is failing:

        for request in world_input:
            requestor_id = request.identifier
            for operation in request.actions:
>               self.command(operation.action, requestor_id, operation.paraneter)
E               AttributeError: 'str' object has no attribute 'action'

This code needs its names fixed to align. I’ll give myself one chance to get this working and then when I fail, roll back and try something smaller. Review the whole picture …

TL;DR
I just removed all the code and verbiage that I produced while not succeeding. You’re welcome.

Nope! I did some stuff but it didn’t work. Roll back. Not even saving the test.

Let’s do a little outline of the input structure we want:

WorldInput
    WorldOperation
        entity_identifier
        WorldActions
            action_name, [parameter_identifier]
            action_name, [parameter_identifier]
            ...
    WorldOperation
        entity_identifier
        WorldActions
            action_name, [parameter_identifier]
            action_name, [parameter_identifier]
            ...
    ...

So WorldInput is a collection of operations, instances of WorldOperation. An operation has an entity identifier and a WorldActions instance (just one), which is a collection of action name and parameter id.

That final object could be an instance of WorldCommand or something.

I don’t usually obsess over names quite this early. I like to wait until they are in place, and perhaps even live with them a while. But here, the names are part of why I didn’t get things clear enough to make my second test work.

What if we name them bottom up?

There are clearly multiple actions against a single entity. The actions all reference the same main entity, though they may involve another entity, such as the object dropped.

I regret even committing these first classes, but since they are all in the test file, they can be changed around readily.

I’m feeling time pressure. I’m about two hours in and know nothing.

Note
It’s important to be aware of one’s feelings. When feeling pressure, we get stressed. When we get stressed, we don’t program as well as when we’re relaxed. I try to use my feelings to remind me to relax or take a break. Here, I just kind of let myself settle down. Plenty of time.

I’m going to try sketching all new classes, this time focusing on the structure, not a test.

I only have the one test anyway. Let’s try this:

EntityAction = namedtuple("EntityAction", "action parameter")

class EntityRequest:
    def __init__(self, entity_identifier):
        self.entity_identifier = entity_identifier
        self.actions = []

    def add_action(self, action: EntityAction):
        self.actions.append(action)

    def identifier(self):
        return self.entity_identifier

    def __iter__(self):
        return iter(self.actions)


class WorldInput:
    def __init__(self):
        self.requests = []

    def __iter__(self):
        return iter(self.requests)

    def add_request(self, request: EntityRequest):
        self.requests.append(request)


class WorldOutput:
    def is_empty(self):
        return True

    def append(self, fetch_result):
        pass


class TestWorldBatch:
    def test_empty_batch(self):
        world = World(10, 10)
        batch_in = WorldInput()
        batch_out = world.process(batch_in)
        assert isinstance(batch_out, WorldOutput)
        assert batch_out.is_empty()

Now to bring process into line with this.

    def process(self, world_input):
        from test_world_batch import WorldOutput
        output = WorldOutput()
        for request in world_input:
            requestor_id = request.identifier
            for operation in request.operations:
                self.command(operation.action, requestor_id, operation.parameter)
            output.append(self.fetch(requestor_id))
        return WorldOutput()

That looks OK, but the proof will come from passing a test. Let’s recreate the one we had, with the names changed.

The test:

    def test_step(self):
        world = World(10, 10)
        bot_id = world.add_bot(5, 5, direction=Direction.EAST)
        request = EntityRequest(bot_id)
        step_action = EntityAction('step', None)
        request.add_action(step_action)
        batch_in = WorldInput()
        batch_in.add_request(request)
        batch_out = world.process(batch_in)
        assert isinstance(batch_out, WorldOutput)

And process needed a bit of tweaking:

class World:
    def process(self, world_input):
        from test_world_batch import WorldOutput
        output = WorldOutput()
        for request in world_input:
            requestor_id = request.identifier
            for action in request:
                self.command(action.action, requestor_id, action.parameter)
            output.append(self.fetch(requestor_id))
        return WorldOutput()

And the identifier method needed to be a property:

class EntityRequest:
    def __init__(self, entity_identifier):
        self.entity_identifier = entity_identifier
        self.actions = []

    @property
    def identifier(self):
        return self.entity_identifier

The test is passing. I’m going to commit, which will wipe out those starting classes and retain these. Commit: two tests running. New classes all in the test file.

Now there really should be something in the WorldOutput. It’ll be whatever a fetch returns.

class World:
    def fetch(self, entity_id):
        return self.map.at_id(entity_id).as_dictionary()

class WorldEntity:
    def as_dictionary(self):
        held = 0 if not self.holding else self.holding.id
        return {'direction': self.direction,
                'held_entity': held,
                'eid': self.id,
                'location': self.location,
                'scent': self.scent,
                'vision': self.vision}

Let’s do a quick check on the location, which with any luck has changed. WorldOutput needs a simple append for now:

class WorldOutput:
    def __init__(self):
        self.results = []

    def is_empty(self):
        return self.results == []

    def append(self, fetch_result):
        self.results.append(fetch_result)

Now, we’ll really want a structured object in that list, but that remains to be defined. We can invade and test location.

    def test_step(self):
        world = World(10, 10)
        bot_id = world.add_bot(5, 5, direction=Direction.EAST)
        request = EntityRequest(bot_id)
        step_action = EntityAction('step', None)
        request.add_action(step_action)
        batch_in = WorldInput()
        batch_in.add_request(request)
        batch_out = world.process(batch_in)
        assert isinstance(batch_out, WorldOutput)
        # invasive just to see how we did
        result = batch_out.results[0]
        assert result['location'] == Location(6, 5)

I am surprised to find that the 0 is out of range. Did I forget to save my results?

class World:
    def process(self, world_input):
        from test_world_batch import WorldOutput
        output = WorldOutput()
        for request in world_input:
            requestor_id = request.identifier
            for action in request:
                self.command(action.action, requestor_id, action.parameter)
            output.append(self.fetch(requestor_id))
        return WorldOutput()

Right. Bad edit. Should be:

class World:
    def process(self, world_input):
        from test_world_batch import WorldOutput
        output = WorldOutput()
        for request in world_input:
            requestor_id = request.identifier
            for action in request:
                self.command(action.action, requestor_id, action.parameter)
            output.append(self.fetch(requestor_id))
        return output  # <===

Test is green. Commit: invasive round trip test finds command executed correctly.

Wow, long article. Time to break, I’m 2 1/2 hours in and tiring. Let’s reflect and sum up.

Reflection / Summary

I think we have a decent first couple of steps now. We have a little action object, just a named tuple, in a collection of actions headed by a particular entity id, in a collection of such things, called WorldInput. We have successfully created a legal one of these and executed it, returning a result that isn’t quite what we want, but was enough like what we want that we could invade it and find that the command had worked as intended.

First time through, I became confused by my own naming scheme, and in fact I think I had the entity identifier at the wrong level in at least some of what I wrote. After a little struggling to see if I could bridge the gap, I wisely rolled back and tried again.

The second time, I thought about the data structure abstractly for a bit and then implemented classes (and a named tuple class) that made sense in those terms. Working from those, a test was easy to write, much like the one that had daunted me earlier. And we quickly adjusted process to use the new classes. And it has successfully returned a fetch dictionary into the output, which may not be quite what we want but is certainly a step on the way.

I think this code is in the usual rough form for first day code, but that the objects are just about standing where we want them to be. I am pleased with the result.

That one bite was too big, and if I’m right that I had the identifier in the wrong class, it was doomed to be too big, I think. But the second bite is the same size, and since we used the learning from the first try to improve the classes, and our understanding, we were able to span the gap readily, even though we identified the need for the property decorator along the way. When the test failed the second time around, it didn’t generate confusion, just the usual “oh that” and fix it up.

Isn’t that kind of just debugging?
Now some practitioners will tell us that when we take a step and write a test we should predict how it’ll fail and, I guess, flail ourselves if we predict wrongly. I don’t work that way, though I will often make the prediction. More commonly, however, I will have put in place a test that is close to what I need, and code that is close to what I need, and then based on what happens, I’ll adjust the test and code until they line up.

If I wanted to put a bad face on that behavior, I’d say that I sort of debug the code until it works. If I wanted to put a good face on it, and I do, I would say that I recognize that my thoughts and typing are imperfect, and that I prefer to get feedback from the compiler and run time to inform me about those imperfections. It would surely be possible to be more careful, and to read my code three times before running the test, and so on, but in fact my tests run every time I stop typing, and if something comes up red, it makes perfect sense to me to decide to look and see what it’s telling me.

What we’re trying to do is to get to working code code, driven by good tests, as rapidly as possible. I think that my practice accomplishes that b granting me the option of reviewing the code or checking feedback from the computer, as may seem best at the time.

What should you do? You should do as you see fit.

I think next time we’ll probably improve the output side of this thing, and perhaps deal with the other actions, and maybe even process more than one bot at a time. That’s a lot: we will almost certainly not get all that in one session. But one never knows, do one? See you then!