Biots Confused
We made some progress yesterday, and I’ll report on that. Before today’s meeting, I am a bit confused about what we should do next. I get a truly scary idea.
First, the news. Then, the confusion. Yesterday, after some blue-sky chats about our biots and their world, we started a test. I was driving and Bryan was following, suggesting, looking things up. I have a tendency, when pairing, to run away with things, probably because I mostly program alone and there’s no one else who needs to keep up, and partly also because I’m a flawed human being. In any case, the first attempt at what I’m about to show you resulted in a test that didn’t work, as one likes to start with, but even though I kept following my nose, it kept not working, turning up more and more issues. We mentioned rolling back once, and agreed to continue.
- Rolling Back
-
When we are taking a small enough step, the kind where a few lines added or changed makes it work, the kind of step we’d like always to take from green to green, if our attempt doesn’t work, you can make a case for just rolling back the implementation part and trying again. We probably won’t make the same mistake again.
-
However, if the step isn’t as small as it might be and our code changes don’t work, I believe it makes sense to work for a while, finding issues and fixing them. Often there are just a few things we forgot and we hook them up and it’s all good. Also often, things are a mess, or the objects aren’t ready for what we tried, or we need some changes before we even get started.
-
In these situations, it is very tempting to just keep digging, even though we are in a hole. We might print some things, we might even use the debugger. Sometimes, it all works out. My guess is, the second time the thought of rolling back comes up, we should roll back. And it wouldn’t hurt to do it the first time.
Yesterday, the second time the thought came up went something like this: I said “We should probably roll back, but I want …”. Then, I am proud to say, and this is my story so this is how it happened, I paused and then said “No, we’ll roll back”, and we did. What you see below is the result of our second attempt.
def test_move(self):
world = BiotWorld()
biot = ClientBiot()
info = world.add(biot)
biot.update(info)
point = info["location"]
assert biot.id == 101
new_info = world.move(biot.id, 10, 0)
biot.update(new_info)
new_point = biot.location
assert new_point == Point(point.x+10, point.y)
You’ll notice, perhaps, that the class names are changing. We now have a WorldBiot, which is the world’s model of the biot, and a ClientBiot, which is the client model of the biot. Even though we are not yet building a real client and real server, we are beginning to keep the concepts separate. I’ll comment on that below, but let’s continue the code review first. This test is showing our current understanding of the flow of a biot moving, as seen from above, as it were, not from the viewpoint of either the Client or the Server.
In the test, we create a world, which will be kind of given in the actual game, some well-known object for clients to talk to. We create a ClientBiot, as might be done in the client, and we add it to the world.
Every message to the world, returns what we’re calling an “info”, which is an evolving structure holding the — well — information that the World gives back when you do things. So far it contains your ID and location, as we’ll soon see. In our test, we record the location provide by the add, because we don’t know where the world will put him. As things evolve, biots will be added at random locations
The biot would typically say self.update(info)
, so in our test we do that, and then check to be sure that the biot has recorded its ID. then it wants to move 10 forward (not the star trek bar, sorry), so we posit a new method on world for that, get the info back, update, and check that the biot has correctly moved.
Now let’s see how that happens.
class BiotWorld:
def __init__(self):
self.biots = Entities()
def add(self, biot):
id = 101
location = Point(10, 10)
world_biot = WorldBiot(id, location)
self.biots.place(id, world_biot)
return {"ID": id, "location": world_biot.location}
def move(self, biot_id, dx, dy):
world_biot = self.biots.contents[biot_id]
location = world_biot.location
world_biot.location = Point(location.x + dx, location.y + dy)
return {"ID": biot_id, "location": world_biot.location}
The world, upon creation, makes an empty Entities collection of biots. Upon add
, it assigns an id, in this case 101, and it computes a random location, Point(10, 10), perhaps not as random as it might be. It creates a WorldBiot, the biot description that belongs to the World server, and places it in the biots collection. We’ll review that soon. Finally, in add
, the World returns an info, which is presently just a dictionary.
- Let me explain
- This story / test is essentially the one we started with yesterday, and it was too big. I don’t quite know what we should have done, but even the second time through, we were on the hairy edge of running out of time and out of my brain power. There should be an object on each side to represent the
info
, but rather than assume yet another one or two classes, I chose just to provide a dictionary. I think I did that rather unilaterally, without much consultation with my pair, Bryan. I was kind of hanging on by my fingernails at that point and had to bridge the gap somehow. We will create server and client info objects soon, I imagine.
In the move
, you can see that we fetch the biot of a given ID (again some hackery because I was so close to losing the thread and running out of Zoom time), get its location, and compute its new location, setting it back into the biot. Then we return the info.
Note the duplication. We envision that every command will return an info
, and kind of suspect that we might always send back all your info and let the client sort it out. An alternative is to have a different info package for each command and that seems tedious and wrong somehow. We’ll find out.
Let’s glance at the Entities class.
class Entities:
def __init__(self):
self.contents = {}
def place(self, biot_id, world_biot):
self.contents[biot_id] = world_biot
Just a cover for a simple dictionary, which is why my hack of fetching the contents worked. I had originally written self.biots[biot_id]
, which of course would not compile. I could have done the trick to make Entities, which is a key-value store, allow the subscripting. I could have added a get
method to Entities. I didn’t have the spoons for either of those things, so I just ripped out the contents and grabbed what I needed. No biscuit. We’ll fix it up, I’m sure.
Now the ClientBiot:
class ClientBiot:
def __init__(self):
self.id = None
self.location = Point(0, 0)
def update(self, info):
self.id = info["ID"]
self.location = info["location"]
That’s all there is to them, so far. Just enough to pass the tests. What else should they know and be able to do? We don’t know yet.
What else?
class WorldBiot:
def __init__(self, id, location):
self.id = id
self.location = location
Simple data record for now, no behavior at all. I imagine that we’ll push some behavior into it as time goes on.
- Keeping Concepts Separate
- As much as I am resisting doing any real client-server stuff, I am of course aware that there must be a strict separation between the world things and the biot things. If the world and client were combined, the world could just send a message to the biot and get information back. It could even set values into it. When, later, we tried to separate things into client and server … it seemed to us yesterday that such direct connections would make it very hard to do the split. So we came up with the notion of the WorldBiot and the ClientBiot, created and named to help us keep them separate.
-
As I write this this morning, I’m not so sure. Oh, yes, it’s a good idea and it surely will be necessary in the client-server mode. But—I wonder—what would happen if we had not done this, and instead went right ahead and let the world access information right out of the biot? What if, instead of passing in the ID, as we are doing in our test, we did pass our single biot class instances to our single world instance? What if we paid as little attention as possible to the client-server notion?
-
When it came time to separate the two, would we be in big trouble? Or would it be straightforward?
Wow. I am so tempted to try it that way. Code this thing up with world and biots together, making no concession at all to the fact that one day it will be client-server, until one day in the future our Customer (us with that hat on) says “Oh, did I mention it needs to be client server?” and we’re all “No, but it should be no problem, tell us more.”
Could we do that? Or would we crash and burn? If we could do it, the early development of the game would surely be much easier, just objects talking with each other, none of this do something then compute an update then do the update then do something …
I feel we should try it. When Bryan and I next meet, we’ll discuss it. Either way, it’ll be interesting and educational, and that, after all, is the point.
I’m glad we had this little chat. See you next time!
- P.S. Confusion
- I mentioned that I was confused. I was, and am. I’m not sure what we should do next. Should we make this into an actual game where you drive a biot around? Should we come up with some way of scripting them? I’m not as clear as I’d like to be on just exactly what this little product is. We did give it a name that allows us to throw it away, of course.
-
We’ll see how the confusion and questions get resolved. It’s part of the process of inventing a product.