reindent code
This commit is contained in:
parent
aa00551266
commit
caf4c58da1
@ -17,54 +17,54 @@ The usual approach to robot behavior design relies on hierarchical state machine
|
|||||||
On to the state-machine approach. First, we'll have a class called Features that abstracts the robot's raw sensor data. For this example, we only care whether the ball is near/far and left/right, so Features will just contain two boolean variables:
|
On to the state-machine approach. First, we'll have a class called Features that abstracts the robot's raw sensor data. For this example, we only care whether the ball is near/far and left/right, so Features will just contain two boolean variables:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Features(object):
|
class Features(object):
|
||||||
ballFar = True
|
ballFar = True
|
||||||
ballOnLeft = True
|
ballOnLeft = True
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, we make the goalkeeper. The keeper's behavior is specified by the `next()` function, which is called thirty times per second by the robot's main event loop (every time the on-board camera produces a new image). The `next()` function returns one of three actions: `"stand"`, `"diveLeft"`, or `"diveRight"`, based on the current values of the Features object. For now, let's pretend that requirement 3 doesn't exist.
|
Next, we make the goalkeeper. The keeper's behavior is specified by the `next()` function, which is called thirty times per second by the robot's main event loop (every time the on-board camera produces a new image). The `next()` function returns one of three actions: `"stand"`, `"diveLeft"`, or `"diveRight"`, based on the current values of the Features object. For now, let's pretend that requirement 3 doesn't exist.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Goalkeeper(object):
|
class Goalkeeper(object):
|
||||||
def __init__(self, features):
|
def __init__(self, features):
|
||||||
self.features = features
|
self.features = features
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
features = self.features
|
features = self.features
|
||||||
if features.ballFar:
|
if features.ballFar:
|
||||||
return 'stand'
|
return 'stand'
|
||||||
|
else:
|
||||||
|
if features.ballOnLeft:
|
||||||
|
return 'diveLeft'
|
||||||
else:
|
else:
|
||||||
if features.ballOnLeft:
|
return 'diveRight'
|
||||||
return 'diveLeft'
|
|
||||||
else:
|
|
||||||
return 'diveRight'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That was simple enough. The constructor takes in the `Features` object; the `next()` method checks the current `Features` values and returns the correct action. Now, how about satisfying requirement 3? When we choose to dive, we need to keep track of two things: how long we need to stay in the `"dive"` state and which direction we dove. We'll do this by adding a couple of instance variables (`self.diveFramesRemaining` and `self.lastDiveCommand`) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of the `next()` function, we check if `self.diveFramesRemaining` is positive; if so, we can immediately return `self.lastDiveCommand` without consulting the `Features`. Here's the code:
|
That was simple enough. The constructor takes in the `Features` object; the `next()` method checks the current `Features` values and returns the correct action. Now, how about satisfying requirement 3? When we choose to dive, we need to keep track of two things: how long we need to stay in the `"dive"` state and which direction we dove. We'll do this by adding a couple of instance variables (`self.diveFramesRemaining` and `self.lastDiveCommand`) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of the `next()` function, we check if `self.diveFramesRemaining` is positive; if so, we can immediately return `self.lastDiveCommand` without consulting the `Features`. Here's the code:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Goalkeeper(object):
|
class Goalkeeper(object):
|
||||||
def __init__(self, features):
|
def __init__(self, features):
|
||||||
self.features = features
|
self.features = features
|
||||||
self.diveFramesRemaining = 0
|
self.diveFramesRemaining = 0
|
||||||
self.lastDiveCommand = None
|
self.lastDiveCommand = None
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
features = self.features
|
features = self.features
|
||||||
if self.diveFramesRemaining > 0:
|
if self.diveFramesRemaining > 0:
|
||||||
self.diveFramesRemaining -= 1
|
self.diveFramesRemaining -= 1
|
||||||
return self.lastDiveCommand
|
return self.lastDiveCommand
|
||||||
|
else:
|
||||||
|
if features.ballFar:
|
||||||
|
return 'stand'
|
||||||
else:
|
else:
|
||||||
if features.ballFar:
|
if features.ballOnLeft:
|
||||||
return 'stand'
|
command = 'diveLeft'
|
||||||
else:
|
else:
|
||||||
if features.ballOnLeft:
|
command = 'diveRight'
|
||||||
command = 'diveLeft'
|
self.lastDiveCommand = command
|
||||||
else:
|
self.diveFramesRemaining = 29
|
||||||
command = 'diveRight'
|
return command
|
||||||
self.lastDiveCommand = command
|
|
||||||
self.diveFramesRemaining = 29
|
|
||||||
return command
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This satisfies all the requirements, but it's ugly. We've added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the `next()` function. Even worse, the structure of the code no longer accurately represents the programmer's intent: the top-level if-statement depends on the state of the robot rather than the state of the world. The intent of the original `next()` function is much easier to discern. (In real code, we could use a state-machine class to tidy things up a bit, but the end result would still be ugly when compared to our original `next()` function.)
|
This satisfies all the requirements, but it's ugly. We've added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the `next()` function. Even worse, the structure of the code no longer accurately represents the programmer's intent: the top-level if-statement depends on the state of the robot rather than the state of the world. The intent of the original `next()` function is much easier to discern. (In real code, we could use a state-machine class to tidy things up a bit, but the end result would still be ugly when compared to our original `next()` function.)
|
||||||
@ -72,41 +72,41 @@ That was simple enough. The constructor takes in the `Features` object; the `nex
|
|||||||
With generators, we can preserve the form of the original `next()` function and keep the bookkeeping only where it's needed. If you're not familiar with generators, you can think of them as a special kind of function. The `yield` keyword is essentially equivalent to `return`, but the next time the generator is called, *execution continues from the point of the last `yield`*, preserving the state of all local variables. With `yield`, we can use a `for` loop to "return" the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:
|
With generators, we can preserve the form of the original `next()` function and keep the bookkeeping only where it's needed. If you're not familiar with generators, you can think of them as a special kind of function. The `yield` keyword is essentially equivalent to `return`, but the next time the generator is called, *execution continues from the point of the last `yield`*, preserving the state of all local variables. With `yield`, we can use a `for` loop to "return" the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class GoalkeeperWithGenerator(object):
|
class GoalkeeperWithGenerator(object):
|
||||||
def __init__(self, features):
|
def __init__(self, features):
|
||||||
self.features = features
|
self.features = features
|
||||||
|
|
||||||
def behavior(self):
|
def behavior(self):
|
||||||
while True:
|
while True:
|
||||||
features = self.features
|
features = self.features
|
||||||
if features.ballFar:
|
if features.ballFar:
|
||||||
yield 'stand'
|
yield 'stand'
|
||||||
|
else:
|
||||||
|
if features.ballOnLeft:
|
||||||
|
command = 'diveLeft'
|
||||||
else:
|
else:
|
||||||
if features.ballOnLeft:
|
command = 'diveRight'
|
||||||
command = 'diveLeft'
|
for i in xrange(30):
|
||||||
else:
|
yield command
|
||||||
command = 'diveRight'
|
|
||||||
for i in xrange(30):
|
|
||||||
yield command
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Here's a simple driver script that shows how to use our goalkeepers:
|
Here's a simple driver script that shows how to use our goalkeepers:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import random
|
import random
|
||||||
|
|
||||||
f = Features()
|
f = Features()
|
||||||
g1 = Goalkeeper(f)
|
g1 = Goalkeeper(f)
|
||||||
g2 = GoalkeeperWithGenerator(f).behavior()
|
g2 = GoalkeeperWithGenerator(f).behavior()
|
||||||
|
|
||||||
for i in xrange(10000):
|
for i in xrange(10000):
|
||||||
f.ballFar = random.random() > 0.1
|
f.ballFar = random.random() > 0.1
|
||||||
f.ballOnLeft = random.random() < 0.5
|
f.ballOnLeft = random.random() < 0.5
|
||||||
g1action = g1.next()
|
g1action = g1.next()
|
||||||
g2action = g2.next()
|
g2action = g2.next()
|
||||||
print "%s\t%s\t%s\t%s" % (
|
print "%s\t%s\t%s\t%s" % (
|
||||||
f.ballFar, f.ballOnLeft, g1action, g2action)
|
f.ballFar, f.ballOnLeft, g1action, g2action)
|
||||||
assert(g1action == g2action)
|
assert(g1action == g2action)
|
||||||
```
|
```
|
||||||
|
|
||||||
... and we're done! I hope you'll agree that the generator-based keeper is much easier to understand and maintain than the state-machine-based keeper. You can grab the full source code below and take a look for yourself.
|
... and we're done! I hope you'll agree that the generator-based keeper is much easier to understand and maintain than the state-machine-based keeper. You can grab the full source code below and take a look for yourself.
|
||||||
|
Loading…
Reference in New Issue
Block a user