Less Wrong
I’m going to continue with Knowledge and see about pushing it into the Machine. Should be pretty easy. (Spoiler: NEVER SAY THAT!)
I’m going to set tired
aside for a moment, because I don’t like the concept anyway, so let’s see what we can do about using Knowledge inside Machine. Here’s Machine as it now stands. I’m not saying I like it.
class Machine:
def __init__(self, bot):
self.bot = bot
self.vision = bot.vision
self.tired = 10
self._update = self.walking_update
self._action = self.walking_action
def state(self, bot):
self.bot = bot
self.vision = bot.vision
self.tired -= 1
self._update()
self._action()
return self
def walking_states(self):
return self.walking_update, self.walking_action
def looking_states(self):
return self.looking_update, self.looking_action
def laden_states(self):
return self.laden_update, self.laden_action
def set_states(self, states):
self._update, self._action = states
def walking_update(self):
if self.tired <= 0:
if self.bot.inventory:
self.set_states(self.laden_states())
else:
self.set_states(self.looking_states())
def walking_action(self):
pass
def looking_update(self):
if self.bot.inventory:
self.tired = 5
self.set_states(self.walking_states())
def looking_action(self):
if self.bot.can_take():
self.bot.take()
def laden_update(self):
if self.bot.has_no_block():
self.tired = 5
self.set_states(self.walking_states())
def laden_action(self):
if self.tired <= 0:
if self.bot.can_drop():
block = self.bot.inventory[0]
self.bot.drop(block)
I think I’ll just say I’m pushing in a Knowledge and see what happens.
class Bot:
def __init__(self, x, y, direction=Direction.EAST):
self.world = None
self.id = None
self.name = 'R'
self.direction_change_chance = 0.2
self.tired = 10
self._knowledge = Knowledge(Location(x, y), direction)
self.state = Machine(self._knowledge)
class Machine:
def __init__(self, knowledge):
self._knowledge = knowledge
self.tired = 10
self._update = self.walking_update
self._action = self.walking_action
@property
def vision(self):
return self._knowledge.vision
def state(self, knowledge):
self._knowledge = knowledge
self.tired -= 1
self._update()
self._action()
return self
Tests are green. I am actually slightly surprised, though I shouldn’t be.
Let’s see where we refer to vision and defer the message to knowledge. Huh! There aren’t any. I remove the property. All green. Commit: convert Machine to use Knowledge.
Now I find myself with a few possible goals, plus a vague wish that I had done this last thing prior to ending the previous article. Since I’m not allowed to go back on my own time line, let’s look to the future.
-
I would like to simplify Bot based on having added Knowledge, hopefully removing some of the methods that forward to Knowledge;
-
I would like to get around to either finishing what I had in mind for Machine, or removing it and replacing with something simpler;
-
I would like to get to Machine returning a list of things to do, and Bot (or a BotController yet to be invented) passing them to world;
-
I would like to get a single object back from World, suitable for updating our knowledge.
In aid of all that, let’s have a look at how World and Bot interact.
class Bot:
def do_something(self):
self.update()
self.state.state(self)
self.move()
def update(self):
pass
I find something curious in Machine:
def laden_action(self):
if self.tired <= 0:
if self._knowledge.can_drop():
block = self._knowledge.inventory[0]
self._knowledge.drop(block)
Knowledge does not know how to drop. How is this even working?
In particular, how is the game working, because I can see it picking up and dropping blocks.
Ah. Somewhere we’re passing in the Bot to the machine, not the Knowledge! We failed to change this:
class Bot:
def do_something(self):
self.update()
self.state.state(self._knowledge)
self.move()
Now a test fails. But not before when we were not wired up correctly. I guess, in a sense, since putting in knowledge was supposed to be a refactoring, we should not expect a failure now, but when it was done wrong, we should have. I’m not sure how to do about that. Anyway, this situation I can surely fix.
I am making things worse, as I go through Machine and change its calls to knowledge to refer to properties rather than functions. This should sort out quickly, I hope.
Ah this is actually good. We have fallen down on this method:
def looking_action(self):
if self._knowledge.can_take:
self._knowledge.take()
Recall that what we want (what we really really want) is for the Machine to return a list of things to do. Let’s start changing it to do that, like this:
def looking_action(self):
if self._knowledge.can_take:
return ['take']
And in do_something
:
def do_something(self):
self.update()
actions = self.state.state(self._knowledge)
for action in actions:
match action:
case 'take':
self.world.take_forward(self)
self.move()
We can’t be quite so cavalier, we need all our action methods to return a list. With that in place, I still have seven tests failing. I am displeased. Still, let’s tick through and see what happens. Ah, I forgot to return the result from the top of Machine:
def state(self, knowledge):
self._knowledge = knowledge
self.tired -= 1
self._update()
return self._action()
Back down to five fails. Forgot to return a list even if the ifs don’t hit. Down to four.
Ah, my tests are passing in the bot to Machine, I haven’t changed them to pass knowledge.
With not too much more effort, mostly changing tests from machine.state(bot)
to machine.state(bot._knowledge)
, I am green, but it can’t possibly be working, as I see it, because:
class Machine:
def laden_action(self):
if self.tired <= 0:
if self._knowledge.can_drop:
block = self._knowledge.inventory[0]
return ['drop']
return []
class Bot:
def do_something(self):
self.update()
actions = self.state.state(self._knowledge)
for action in actions:
match action:
case 'take':
self.world.take_forward(self)
case _:
assert 0, f'no case {action}'
self.move()
First of all, that .inventory[0]
really ought to fail, and second, we’re not handling ‘drop’ in the match, and that ought to fail.
The game does fail, as expected. Are we missing a test for dropping the block?
First, I find some more places where Machine is being passed a bot, not a knowledge, so I add assertions to the class to check. That turns up some tests that need changing.
Arrgh, my only test from drop does an explicit drop. Has no real choice:
def test_laden_goes_walkabout_after_drop(self):
bot = Bot(5, 5)
machine = Machine(bot._knowledge)
entity = Block(3, 3)
bot.receive(entity)
vision_list = [('R', 5, 5)]
bot.vision = vision_list
bot.direction = Direction.EAST
machine.set_states(machine.laden_states())
machine.state(bot._knowledge)
assert bot.has_block()
assert machine._action == machine.laden_action
bot.remove(entity)
machine.state(bot._knowledge)
assert not bot.has_block()
assert machine._action == machine.walking_action
I wonder if we could change those calls to state to call do_something
instead. No, we’d need a world. We could check the result and see if there is a drop
. I change the test to this:
def test_laden_goes_walkabout_after_drop(self):
bot = Bot(5, 5)
machine = Machine(bot._knowledge)
entity = Block(3, 3)
bot.receive(entity)
vision_list = [('R', 5, 5), ('B', 6, 6)]
bot.vision = vision_list
bot.direction = Direction.EAST
machine.tired = 0
machine.set_states(machine.laden_states())
assert bot.has_block()
assert machine._action == machine.laden_action
actions = machine.state(bot._knowledge)
assert 'drop' in actions
machine._knowledge.remove(machine._knowledge._entity)
_ = machine.state(bot._knowledge)
assert machine._action == machine.walking_action
We are green and the game is working. I am not satisfied, and I am seriously tired. I will commit this, but with a caveat.
Commit: doing take and drop based on returns from machine, drop is somewhat hacked. Getting closer, everything works. Tests need improvement, and something needs to be done about round-tripping rather than do the full bot/world thing.
Summary
This was a bit raggedy. Everything seemed to work at first, but some of that was artifacts in how the tests work, in particular some of them kind of work behind the scenes to set the objects up for state testing and the like, and much of that logic changed somewhat when we moved to Knowledge.
I was never in trouble … but I worked from 1145 to 1335 with no commits. That is Very Bad. There were a few points in there when we could have committed, everything was green, but I felt that the tests weren’t telling the whole truth.
Raggedy.
We are a good way along to having the machine return a list of things to do, although we’ll need to beef that up into a sort of Command object, so that we can pass out the block to be dropped. Right now, Bot reaches down to get it. the responses of World are still firing direct through Bot and into its innards, which is also Very Much Not So Good. We’ll need to work on that in a separate phase for sure.
I am not impressed with my work over the past handful of sessions. Mostly it has gone smoothly, but the paths I’ve chosen seem to me to have been roundabout and not in a good direction, although commits have been high and tests green. And this afternoon … just raggedy. But, I think a good result, as we are running the Machine on Knowledge, and have begun returning a list of actions.
I think a wise person, at this point, would improve some tests. What do I mean by that? Good question:
I think we should isolate logic from simple edits. Test that World and Bot interact as we desire with respect to inventory (and location and direction, probably). Test separately that the Machine / Knowledge combination make the right decisions, with as little reference to the edits from World as possible.
Then we should work quickly (and separately) toward passing the command list to World and getting new information for the Bot back.
For now, I need a break. As do you, I’m sure.