BOTS Vision
This morning, up at 0650 for some reason, I think I’ll push a bit on the “vision” idea. GeePaw arrives and helps out.
My “vision” idea was to make a little text pattern out of the vision data we get, which consists of an object’s name ‘R’ for roBot and ‘B’ for Block, as of now. Considering the nine cells around the current position, I’m imagining a little 9-character string, representing what’s around us, like this:
B_B
_R_
___
So there we are, and to our north there are two blocks, with an empty space between them. Nothing else in view.
My theory is that it would be convenient to represent patterns around the Bot, and use them to decide whether to take or drop things. I have dropping in mind i particular, because I am aiming toward a simple rule that says, roughly “if you are carrying a block and you see a block near you, put your block beside that block”. We think that rule will cause the bot or bots to collect all the blocks into clumps.
I have some tests for “vision”, which I started the other day.
class TestVision:
def test_hookup(self):
assert True
def test_nothing_near(self):
world = World(10, 10)
bot = Bot(5, 5)
bot.direction_change_chance = 0.0
world.add(bot)
bot.vision = None
bot.move()
assert bot.location == Point(6, 5)
vision = bot.vision
assert ('R', 6, 5) in vision
def test_three_blocks_near(self):
from block import Block
world = World(10, 10)
world.add(Block(4, 4))
world.add(Block(6, 6))
world.add(Block(4, 5))
bot = Bot(6, 5)
bot.direction = Direction.WEST
bot.direction_change_chance = 0.0
world.add(bot)
bot.vision = None
bot.move()
assert bot.location == Point(5, 5)
vision = bot.vision
assert ('R', 5, 5) in vision
assert ('B', 4, 5) in vision
assert ('B', 6, 6) in vision
assert ('B', 4, 4) in vision
def test_a_pattern(self):
vision ='B_B'\
'_RB'\
'B__'
pattern = r'B_..R....'
result = re.search(pattern, vision)
assert result
pattern = r'.B._RBBBB'
result = re.search(pattern, vision)
assert not result
That last one is the only one that begins to consider the pattern idea. I was considering regular expressions as a way of searching them. This morning I’m thinking of a class or two of my own. Let’s try a test for that, to try to solidify “a class or two of my own” into something more concrete.
def test_vision_class_exists(self):
vision_list = [('R', 5, 5), ('B', 4, 4), ('B', 6, 4)]
vision = Vision(vision_list)
That’s more than enough to force me to make the class. I’ll leave it in the test file for now.
class Vision:
def __init__(self, vision_list):
pass
Test passes. Let’s rename it and extend it. I don’t really love tests that just force creation of a class. I like tests that test something.
def test_vision_pattern(self):
vision_list = [('R', 5, 5), ('B', 4, 4), ('B', 6, 4)]
vision = Vision(vision_list)
pattern = 'B_B_R____'
assert vision.matches(pattern)
I could use Fake It Till You Make It Here, but I’m feeling smart enough to code the method. We’ll see about that.
I write this using Wishful Thinking:
def matches(self, pattern):
index = 0
for y in (-1, 0, 1):
for x in (-1, 0, 1):
item = self.vision_at(x, y)
if pattern[index] != item:
return False
index += 1
return True
That requires vision_at
:
def vision_at(self, x, y):
for name, vx, vy in self.vision_list:
if vx == x and vy == y:
return name
return '_'
Test fails. I believe that will be because I’m not taking the bot location into account. The test result is nearly useless, since matches
just returns False and we do not know why.
But I think I know why and therefore I’ll try to fix it. This may be the beginning of a debacle.
def matches(self, pattern, location: Point):
index = 0
for y in (-1, 0, 1):
for x in (-1, 0, 1):
check_location = Point(x, y) + location
item = self.vision_at(check_location.x, check_location.y)
if pattern[index] != item:
return False
index += 1
return True
Test still fails and I still do not know why. I resort to a print. That tells me that the incrementing of index
was tabbed out too far. This passes:
class Vision:
def __init__(self, vision_list):
self.vision_list = vision_list
def matches(self, pattern, location: Point):
index = 0
for y in (-1, 0, 1):
for x in (-1, 0, 1):
check_location = Point(x, y) + location
item = self.vision_at(check_location.x, check_location.y)
if pattern[index] != item:
return False
index += 1
return True
def vision_at(self, x, y):
for name, vx, vy in self.vision_list:
if vx == x and vy == y:
return name
return '_'
Let’s commit the test file and reflect a bit.
Reflection
The first thing that leaps out at me is that the world, in creating the vision list, indexes right around the bot location -1, 0, 1 etc, and returns the world coordinates of the things seen, and then in our Vision object we use the bot location to turn those back into -1, 0, 1 etc., for convenient use. You might argue that World should send us what we want.
I believe that Bryan and I actually discussed this, and decided that since, in the learning situation, we want the learner to encounter troublesome issues mating things up, we’d leave it in this inconvenient form.
The OG Arrives
GeePaw Hill popped in at this point. I brought him up to speed. We worked a bit more on the vision pattern, resulting in these tests:
def test_a_pattern(self):
vision ='B_B'\
'_RB'\
'B__'
pattern = r'B_..R....'
result = re.search(pattern, vision)
assert result
pattern = r'.B._RBBBB'
result = re.search(pattern, vision)
assert not result
def test_vision_pattern(self):
vision_list = [('R', 5, 5), ('B', 4, 4), ('B', 6, 4)]
vision = Vision(vision_list)
pattern = 'B_B_R____'
assert vision.matches(pattern, Point(5, 5))
def test_vision_wildcard_pattern(self):
vision_list = [('R', 5, 5), ('B', 4, 4), ('B', 6, 4)]
vision = Vision(vision_list)
pattern = 'B_???????'
assert vision.matches(pattern, Point(5, 5))
def test_vision_pattern_does_not_match(self):
vision_list = [('R', 5, 5), ('B', 4, 4), ('B', 6, 4)]
vision = Vision(vision_list)
pattern = 'B_B_R_B__'
assert not vision.matches(pattern, Point(5, 5))
def test_bot_drop_decision(self):
vision_list = [('R', 5, 5), ('B', 4, 4)]
bot = Bot(5, 5)
bot.vision = vision_list
bot.direction = Direction.NORTH
assert bot.near_block()
That last one tests whether the bot is near a block, which is now used to decide whether to drop:
def do_something(self):
if self.state == "walking":
...
elif self.state == "looking":
...
elif self.state == "laden":
if self.tired <= 0:
if self.near_block():
block = self.inventory[0]
self.world.drop_forward(self, block)
if block not in self.inventory:
self.tired = 5
self.state = "walking"
self.move()
def near_block(self):
vision = Vision(self.vision)
p1 = 'B_???????'
if vision.matches(p1, self.location):
self.direction = Direction.NORTH
return True
return False
We just recognize one pattern so far, a block to your northwest. If there is one, we drop the block. Let me extend this method right now, sans tests, because I want to.
def near_block(self):
vision = Vision(self.vision)
p1 = 'B_???????'
if vision.matches(p1, self.location):
self.direction = Direction.NORTH
return True
if vision.matches('__B??????', self.location):
self.direction = Direction.NORTH
return True
if vision.matches('BBB??????', self.location):
self.direction = Direction.WEST
return True
return False
Note that there is a “trick” going on here. If we run up against three blocks in a row, we turn west and drop a block, which will give us a pattern like ‘BBBBR?????’, an L quad on its side. I expect these three patterns will allow for some pretty good clumping of blocks over time as the bot runs. I’m sure we’ll want some tuning, and certainly we need to use the Vision in picking up, because we’ll always pick up a block even if it is in a clump.
Hill and I made some other changes of no import, with one exception: we were again confused by whether north is up or down, and we have changed the definition “One Last Time”:
Direction.NORTH = Direction(0, -1)
Direction.EAST = Direction(1, 0)
Direction.SOUTH = Direction(0, 1)
Direction.WEST = Direction(-1, 0)
Direction.CENTER = Direction(0, 0)
Direction.NORTH_WEST = Direction(-1, -1)
Direction.NORTH_EAST = Direction(1, -1)
Direction.SOUTH_WEST = Direction(-1, 1)
Direction.SOUTH_EAST = Direction(1, 1)
Direction.ALL = [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST]
Direction.EVERY = [Direction.NORTH_WEST, Direction.NORTH, Direction.NORTH_EAST,
Direction.WEST, Direction.CENTER, Direction.EAST,
Direction.SOUTH_WEST, Direction.SOUTH, Direction.SOUTH_EAST]
We added the corners so that we could use them in the Vision code:
class Vision:
def __init__(self, vision_list):
self.vision_list = vision_list
def matches(self, pattern, location: Point):
index = 0
for direction in Direction.EVERY:
check_location = Point(direction.x, direction.y) + location
item = self.vision_at(check_location.x, check_location.y)
pattern_item = pattern[index]
if pattern_item != '?' and pattern_item != item:
return False
index += 1
return True
def vision_at(self, x, y):
for name, vx, vy in self.vision_list:
if vx == x and vy == y:
return name
return '_'
So this is all green and committed. More important, I think, is that GeePaw made some solid observations about the granularity of our tests. For example, there are a handful of tests of the Bot moving north, east, west, and so on. Tests for “up against the wall, [DELETED]”, and so on. We discussed it for a bit and the essence, in my interpretation, is that if Point and location and direction were all doing their job, we could have some tests on point and direction, and perhaps as few as one test on Bot motion, to ensure that its location gets changed when it moves (and not when it doesn’t?).
Part of making that happen will be tricky, in that location
is a member variable in the entity, and we probably really want it to be a property of the World (this entity is at this location) and if that information is kept in World and in the entity, there is the possibility that they’ll go out of sync. Something needs to be done about that, preferably not involving a circular reference or an Observer pattern.
I do not know what that something is.
By the way, the Bot has been running a while, since I made the change above, and look at the clumping:
Woot!
So, that’s my report for the day. Hill and I plan to meet tomorrow if we both wake up early enough in the morning.
See you then!