From caf4c58da115b56e56f8a6197415bd9c1c96309c Mon Sep 17 00:00:00 2001 From: Colin McMillen Date: Thu, 1 Jul 2021 20:35:35 -0400 Subject: [PATCH] reindent code --- .../blog/20070502-robot-behaviors-python.md | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/content/blog/20070502-robot-behaviors-python.md b/content/blog/20070502-robot-behaviors-python.md index b493d75..209a153 100644 --- a/content/blog/20070502-robot-behaviors-python.md +++ b/content/blog/20070502-robot-behaviors-python.md @@ -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: ```python - class Features(object): - ballFar = True - ballOnLeft = True +class Features(object): + ballFar = 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. ```python - class Goalkeeper(object): - def __init__(self, features): - self.features = features +class Goalkeeper(object): + def __init__(self, features): + self.features = features - def next(self): - features = self.features - if features.ballFar: - return 'stand' + def next(self): + features = self.features + if features.ballFar: + return 'stand' + else: + if features.ballOnLeft: + return 'diveLeft' else: - if features.ballOnLeft: - return 'diveLeft' - else: - return 'diveRight' + 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: ```python - class Goalkeeper(object): - def __init__(self, features): - self.features = features - self.diveFramesRemaining = 0 - self.lastDiveCommand = None +class Goalkeeper(object): + def __init__(self, features): + self.features = features + self.diveFramesRemaining = 0 + self.lastDiveCommand = None - def next(self): - features = self.features - if self.diveFramesRemaining > 0: - self.diveFramesRemaining -= 1 - return self.lastDiveCommand + def next(self): + features = self.features + if self.diveFramesRemaining > 0: + self.diveFramesRemaining -= 1 + return self.lastDiveCommand + else: + if features.ballFar: + return 'stand' else: - if features.ballFar: - return 'stand' + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - self.lastDiveCommand = command - self.diveFramesRemaining = 29 - return command + command = 'diveRight' + 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.) @@ -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: ```python - class GoalkeeperWithGenerator(object): - def __init__(self, features): - self.features = features - - def behavior(self): - while True: - features = self.features - if features.ballFar: - yield 'stand' +class GoalkeeperWithGenerator(object): + def __init__(self, features): + self.features = features + + def behavior(self): + while True: + features = self.features + if features.ballFar: + yield 'stand' + else: + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - for i in xrange(30): - yield command + command = 'diveRight' + for i in xrange(30): + yield command ``` Here's a simple driver script that shows how to use our goalkeepers: ```python - import random - - f = Features() - g1 = Goalkeeper(f) - g2 = GoalkeeperWithGenerator(f).behavior() - - for i in xrange(10000): - f.ballFar = random.random() > 0.1 - f.ballOnLeft = random.random() < 0.5 - g1action = g1.next() - g2action = g2.next() - print "%s\t%s\t%s\t%s" % ( - f.ballFar, f.ballOnLeft, g1action, g2action) - assert(g1action == g2action) +import random + +f = Features() +g1 = Goalkeeper(f) +g2 = GoalkeeperWithGenerator(f).behavior() + +for i in xrange(10000): + f.ballFar = random.random() > 0.1 + f.ballOnLeft = random.random() < 0.5 + g1action = g1.next() + g2action = g2.next() + print "%s\t%s\t%s\t%s" % ( + f.ballFar, f.ballOnLeft, 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.