The Repo on GitHub

We have been ignoring the requirement that this must be a multi-client one-server program. We have done nothing about that, on purpose. I think we need a bit more thinking, if not doing, on the subject.

An overall objective for this little project is that it will break into two parts, a server that maintains the World and its contents, and clients, which can spawn robots (and maybe other things) into the world. The robots are programmed by the client-side people to try to thrive in the World. (Of course, nothing prevents someone from building a client that is wholly or partly controlled by a human pressing buttons and twisting dials, although they may find that the completely automated robots get a lot more turns. We don’t know, we haven’t even begun to figure that part out.)

That is not an oversight: it is intentional. We propose not to build a separate server until rather late in the project. We want to wait much longer than a reader might intuitively think we “should”. We know that at least some people writing things like this start very early on with the server and the client, and then develop from there. We want to explore what happens when we lean the other way, leaving as much of the server-client stuff until later on.

We are aware that we may suffer greatly for waiting too long … but we don’t think that will happen. Either way, it’ll make for a great learning experience.

However.

We are certainly aware of the client-server requirement, and it seems reasonable to behave roughly like this:

  1. Try to avoid doing things that will make client-server impossible;
  2. When we want to do something that seems difficult or impossible in client-server, try to find ways to make it possible;
  3. Defer working on things like JSON as long as possible;
  4. Defer building separate tasks or programs as long as possible;
  5. Try to avoid contaminating one “side” with the other “side’s” information;
  6. Stay at least peripherally aware of the decisions we’re making that affect the client-server issue;
  7. Avoid being slowed down by client-server requirements;

And so on. We want to avoid the client-server work for as long as possible, and to avoid doing anything that will seriously damage our ability to do that work later.

I have concerns.

As things are set up now, the cycle for a Bot is roughly this:

The World cycles through all the Bots, and calls the do_something method on the Bot.

The Bot decides what to do. Our current one uses a little state machine. The bot can do a few things:

  1. change the direction it is facing (North, East, South, West);
  2. try to take an object that is in front of it;
  3. try to drop something it is carrying, in front of it;
  4. step forward.

As coded, our Bots make decisions about their state and may take or drop, and they always take a step.

I think we have some code that is incompatible with client-server. I would like to be wrong, but if I’m right, I think we need to do something about it, or at least have a plan for it. I’ve asked for an “all-hands” meeting to discuss the issue, and this article constitutes my thoughts in preparation for that meeting.

The issue is this: In the Bot, starting from do_something, which is sent from World, we send messages to World. Here’s the logic for moving:

class Bot:
    def do_something(self):
        self.state()
        self.move()

    def move(self):
        if random.random() < self.direction_change_chance:
            self.change_direction()
        step_succeeded = self.took_a_step()
        if not step_succeeded:
            self.change_direction()

    def took_a_step(self):
        old_location = self.location
        self.step()
        return self.location != old_location

    def step(self):
        self.world.step(self)
        self.tired -= 1

Our current move logic sends a message to self.world while running a call from World. Furthermore, it immediately checks the result of that call (self.location!=old_location). In the client-server mode, that amounts to a network access, a GET or something from the World, with what appears to be an immediate return.

So, I don’t know as much about network calls as I will later on, but that says to me that there is at least some kind of asynchronous thing happening there, and that as written it seems to me to place a serious constraint on how we will ultimately do the client-server thing.

Naively, and believe me, that is the only way I can do it, I see two ways that client-server might ultimately work.

Bot clients initiate actions

The client maintains some number of Bots, figures out their next actions, and sends a message to the World server saying what those actions are. Some time later, during which the client may or may not be doing something else, the World returns a message saying what the new situation is, with, for example, a bot location for each bot, a field of vision for each, sensor results, damage report, weather, whatever all the server provides. Then the client makes another plan and repeats.

World server polls for actions

Alternatively, the World might poll the Bot clients, sending them current status, at which point they would figure out their plan and return it as a response.

It seems to me that the first scheme is more likely the sensible one but I have no real experience in doing anything much more sophisticated than a little web scraping or sending queries and actions to a web server. I imagine that someone knows better than I do what needs to be done.

But in either of the cases I’ve sketched above, there is pretty clear hiatus between the bot side having a plan, and the world side providing the results of that plan. The client side could, I guess, send one big message with actions for each of its bots, or a message per bot. I’d think that the former would be better as a design and the latter might be easier for the client, since it will surely plan each bot more or less independently.

Let’s see what references we have to the self.world in Bot.

class Bot:
    def __init__(self, x, y, direction=Direction.EAST):
        self.world = None
        self.id = None
        self.name = 'R'
        self.location = Location(x, y)
        self.direction = direction
        self.direction_change_chance = 0.2
        self.inventory = []
        self._vision = None
        self.tired = 10
        self.state = self.walking

    def laden(self):
        if self.tired <= 0:
            if self.can_drop():
                block = self.inventory[0]
                self.world.drop_forward(self, block)
                if block not in self.inventory:
                    self.tired = 5
                    self.state = self.walking

    def scan(self):
        return self.world.scan(self)

    def step(self):
        self.world.step(self)
        self.tired -= 1

    def take(self):
        self.world.take_forward(self)

I believe that no use is presently made of scan in actual play, but there are tests for it and an implementation in world, that returns a list of entities at locations or something like that.

Presently a Bot gets its reference to World injected into it, along with its ID, in the World’s add method:

class World:
    def add(self, entity):
        entity.world = self
        World.next_id += 1
        entity.id = World.next_id
        self.map.place(entity)

This code has a parallel issue to that in Bot: it seems to be (and currently really is) putting values into the Bot. The good news is that it is not then reading out other information from the Bot, so it isn’t necessarily a hidden network call.

Ideas

I have some ideas, p-baked1 for small p:

  • Begin to formalize the list of actions that the world can take on a client’s behalf, such as “take”, “drop”, “move”, “scan”, and so on;

  • Begin to formalize the responses to those actions, such as “add X to Bot 5’s inventory”, “remove Y from Bot 7’s inventory”, “set Bot 9’s location to X1, Y1”, “Add 100 to Bot 4’s money”, …, whatever we need.

  • Begin to think in terms of these messages, not as direct method calls, but as sort of a command pattern data item with a very and some information. The sort of thing that could be sent over a connection.

  • Stop making decisions in Bot based on thinking we have an immediate response from the world. Move to more of a question-answer format.

  • Perhaps (even lower “p” than usual?) make simple proxy objects for World and Bot that buffer calls and responses. (Vague? Yes. But I feel an idea aborning here.)

Bottom Line

I think there are places in our current design that are incompatible with the client-server requirement and that we should make some small adjustments to ensure that we don’t strangle ourselves.

A counter-case could be made that we should pretend we are even less intelligent than we actually are, and reap the whole darn whirlwind later. I think we are sufficiently unintelligent and that we need not pretend to be worse.

So I’ve put my thoughts down here, asked my team for a meeting, and maybe I’ll spike something later. For now … I’ll push this article and take a break.



  1. The existence of half-baked ideas implies the existence of partially baked ideas, p-baked, for 0 <= p <= 1. Half-baked ideas are thus merely a special named set of p-baked ideas, where p = 0.5.