3
\$\begingroup\$

A while ago I posted this question about worker AI and Job Queues for a strategy game. I have posted other questions about the Job Queue, and now I would like to post this revised question about specifically the movement class for the workers in the game. I think it would make for a good code review.

The basic idea is that each turn, the update method is triggered which computes the destination floor of a worker based on its current state and the available jobs on different floors. The DwarfMovement class computes the floor to move to, and the Movement class handles the basic movement itself. Enemy, animal, and dwarf movement classes all inherit from the Movement class.

I've spent a lot of time cleaning up this code, but I am sure there are still things that I can improve. I know it is a lot of code, but please feel free to comment on small things. Those types of answers are very helpful too.

DTMovement.h:

#import <Foundation/Foundation.h>

@interface DTMovement : NSObject <NSCoding>

-(id) initWithWorldSize:(CGSize)worldSize;

#pragma mark - Position Properties
@property CGPoint currentPosition;
@property CGPoint destinationPosition;
@property int currentFloor;
@property int destinationFloor;

#pragma mark - Update
-(void) doMovement;

#pragma mark - Advanced Movement
-(void) doIdleMovement;
-(void) doVerticalMovement;
-(void) doFloorMovement;
-(void) pickRandomDestinationOnCurrentFloor;

#pragma mark - Simple Movement
-(void) moveUp;
-(void) moveDown;
-(void) moveLeft;
-(void) moveRight;

#pragma mark - Postion Calculations
-(int) currentFloorByPosition;
-(int) closestFloor:(NSMutableArray *)possibleFloors;

-(void) calculateFloorExitPositionByFloor;
-(void) calculateDestinationPositionByFloor;

-(BOOL) isAtVerticalDestinationPosition;
-(BOOL) isAtFloorDestinationPosition;
-(BOOL) shouldMoveUp;
-(BOOL) shouldMoveRight;

#pragma mark - Tower Change
-(void) transitionToNewTower;

@end

DTMovement.m:

#import "DTMovement.h"
#import "DTTowerFloor.h"

static int const kMinVerticalStep = 18;
static int const kMinHorizontalStep = 18;

@implementation DTMovement {
    CGSize _worldSize;
}

-(id) initWithWorldSize:(CGSize)worldSize {
    self = [super init];
    if (self) {
        _worldSize = worldSize;
    }
    return self;
}

#pragma mark - Update
-(void) doMovement {
    //overridden by subclasses
}

#pragma mark - Vertical Movement
-(void) doVerticalMovement {
    //the destination floor could change so this needs to update each tick
    [self calculateDestinationPositionByFloor];

    //once close enough to the destination, jump to the destination
    if ([self isAtVerticalDestinationPosition]) {
        [self arriveAtDestinationFloor];
    } else {
        if ([self shouldMoveUp]) {
            [self moveUp];
        } else {
            [self moveDown];
        }
    }
}
-(BOOL) isAtVerticalDestinationPosition {
    int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
    int minimumDistance = kMinVerticalStep;
    return (!(distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance));
}
-(BOOL) shouldMoveUp {
    int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
    return distanceFromDestination < 0;
}
-(void) arriveAtDestinationFloor {
    //overridden by subclasses
}

#pragma mark - Horizontal Movement
-(void) doFloorMovement {
    //overridden by subclasses
}
-(BOOL) isAtFloorDestinationPosition {
    int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
    int minimumDistance = kMinHorizontalStep;
    return (!(distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance));
}
-(BOOL) shouldMoveRight {
    int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
    return distanceFromDestination < 0;
}

#pragma mark - Idle Movement
-(void) doIdleMovement {
    //move randomly left and right, prevent moving past the walls of the floor
    CGPoint tempPosition;
    int moveLeftOrRight = arc4random_uniform(2);
    if (moveLeftOrRight == 0) {
        tempPosition = CGPointMake(self.currentPosition.x + kMinHorizontalStep, self.currentPosition.y);
    } else if (moveLeftOrRight == 1) {
        tempPosition = CGPointMake(self.currentPosition.x - kMinHorizontalStep, self.currentPosition.y);
    }

    //bounds checking
    if (tempPosition.x < - _worldSize.width/2) {
        tempPosition.x = -_worldSize.width/2 + kMinHorizontalStep;
    }
    if (tempPosition.x > _worldSize.width/2){
        tempPosition.x = _worldSize.width/2 - kMinHorizontalStep;
    }

    self.currentPosition = tempPosition;
}

#pragma mark - Simple Movement
-(void) moveUp {
self.currentPosition = CGPointMake(0, self.currentPosition.y + _worldSize.height / kMinVerticalStep);
}
-(void) moveDown {
    self.currentPosition = CGPointMake(0, self.currentPosition.y - _worldSize.height / kMinVerticalStep);
}
-(void) moveLeft {
    self.currentPosition = CGPointMake(self.currentPosition.x - _worldSize.width / kMinHorizontalStep, self.currentPosition.y);
}
-(void) moveRight {
    self.currentPosition = CGPointMake(self.currentPosition.x + _worldSize.width / kMinHorizontalStep, self.currentPosition.y);
}

#pragma mark - Position Calculations
-(int) closestFloor:(NSMutableArray *)possibleFloors {
    int currentFloor = [self currentFloorByPosition];
    int destinationFloor = 0;
    int bestCount = 1000; //to guarantee it gets assigned on the first try
    int currentCount = 0;
    for (DTTowerFloor *floor in possibleFloors) {
        //find out what floor has the lowest count of distance
        if (currentFloor > floor.floorNumber) {
            for (int i = currentFloor; i > floor.floorNumber; i--) {
                currentCount++;
            }
        } else {
            for (int i = currentFloor; i < floor.floorNumber; i++) {
                currentCount++;
            }
        }
        //set the destination to the floor with lowest count
        if (currentCount < bestCount) {
            destinationFloor = floor.floorNumber;
            bestCount = currentCount;
        }
        currentCount = 0;
    }
    return destinationFloor;
}
-(int) currentFloorByPosition {
    int floorSize = _worldSize.height/6;
    return self.currentPosition.y / floorSize;
}
-(void) calculateDestinationPositionByFloor {
    self.destinationPosition = CGPointMake(0, self.destinationFloor * _worldSize.height/6 - _worldSize.height/6/3);
}
-(void) calculateFloorExitPositionByFloor {
    int currentFloor = [self currentFloorByPosition];
    int floorY = currentFloor * _worldSize.height/6 - _worldSize.height/6/3;
    self.destinationPosition = CGPointMake(0, floorY);
}
-(void) pickRandomDestinationOnCurrentFloor {
    int randomNumber = arc4random_uniform(4) + 1;
    int randomXPosition = (_worldSize.width/3)/randomNumber;
    self.destinationPosition = CGPointMake(randomXPosition, self.currentPosition.y);
}

#pragma mark - Tower Change
-(void) transitionToNewTower {
    //overridden by subclasses
}

#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        _currentPosition = [aDecoder decodeCGPointForKey:@"currentPosition"];
        _destinationPosition = [aDecoder decodeCGPointForKey:@"destinationPosition"];
        _currentFloor = [aDecoder decodeIntForKey:@"currentFloor"];
        _destinationFloor = [aDecoder decodeIntForKey:@"destinationFloor"];
        _worldSize = [aDecoder decodeCGSizeForKey:@"worldSize"];
    }
    return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeCGPoint:self.currentPosition forKey:@"currentPosition"];
    [aCoder encodeCGPoint:self.destinationPosition forKey:@"destinationPosition"];
    [aCoder encodeInt:self.currentFloor forKey:@"currentFloor"];
    [aCoder encodeInt:self.destinationFloor forKey:@"destinationFloor"];
    [aCoder encodeCGSize:_worldSize forKey:@"worldSize"];
}

@end

DTDwarfMovement.h:

#import <Foundation/Foundation.h>
#import "DTMovement.h"
#import "DTDwarfMovementState.h"

@interface DTDwarfMovement : DTMovement <NSCoding>

-(id) initWithWorldSizeForCharacter:(CGSize)worldSize;

@property DwarfMovementState movementState;

@property BOOL isTiredOrHungry;

-(void) acceptFloorList:(NSMutableArray *)floorList;
-(void) acceptFilteredFloorListForWork:(NSMutableArray *)filteredFloorList;

@end

This is most of the AI of the worker.

DTDwarfMovement.m:

#import "DTDwarfMovement.h"
#import "DTTowerFloor.h"

/*
 Each turn if the dwarf is in a correct state, position is updated in the doMovement method
 Lists of valid floors are loaded in before the destination is computed
 There are different computations for special cases such as hunger, idle, playing
 Movement is computed in the super class, and it is restricted to up down left and right
 Typically when at a destination, state changes
 Some states are advanced by the Tower or TowerFloor
 */

@implementation DTDwarfMovement {
    NSMutableArray *_floorList;
    NSMutableArray *_floorListForWork;
}

-(id) initWithWorldSizeForCharacter:(CGSize)worldSize {
    self = [super initWithWorldSize:worldSize];
    if (self) {
        _floorList = [[NSMutableArray alloc]init];
        _floorListForWork = [[NSMutableArray alloc]init];
    }
    return self;
}

#pragma mark - Floor List Handling
-(void) acceptFloorList:(NSMutableArray *)floorList {
    _floorList = floorList;
}
-(void) acceptFilteredFloorListForWork:(NSMutableArray *)filteredFloorList {
    _floorListForWork = filteredFloorList;
}

#pragma mark - Checking For Jobs
-(BOOL) checkFloorsForJobs {
    for (DTTowerFloor *floor in _floorListForWork) {
        if ([floor areJobSlotsAvailable]) {
            return YES;
        }
    }
    return NO;
}

#pragma mark - Update Movement
-(void) doMovement {
    switch (self.movementState) {

        //in the idle state, if not hungry or tired, try to choose a job, if no job found, idle
        case DwarfMovementStateIdle:
            if (!self.isTiredOrHungry) {
                [self tryToChooseAJobWhileIdle];
            }
            break;

        //once at the floor, if no jobs available, idle
        case DwarfMovementStateAtDestinationFloor:
            if (![self checkFloorsForJobs]) {
                self.movementState = DwarfMovementStateIdle;
            }
            break;

        //this is the state when a dwarf first finds a job
        case DwarfMovementStateNeedsMoving:
            [self calculateFloorExitPositionByFloor];
            self.movementState = DwarfMovementStateToFloorExit;
            break;

        //basic movement between floors and to positions on floors
        case DwarfMovementStateAtFloorExit:
            self.movementState = DwarfMovementStateToFloor;
            break;
        case DwarfMovementStateToFloor:
            [self doVerticalMovement];
            break;
        case DwarfMovementStateToJobPosition:
            [self doFloorMovement];
            break;
        case DwarfMovementStateToFloorExit:
            [self doFloorMovement];
            break;
        case DwarfMovementStateToRandomPositionForEating:
            [self doFloorMovement];
            break;
        case DwarfMovementStateToRandomPositionForResting:
            [self doFloorMovement];
            break;

        //just randomly moves around, mostly up and down as the destination floor constantly changes
        case DwarfMovementStatePlaying:
            [self handlePlayingMovement];
            break;

        //deal with specific movement states
        case DwarfMovementStateToStockpile:
            [self pickDestinationFloorForStockpile];
            [self doMovementForMovementState:DwarfMovementStateToStockpile];
            break;
        case DwarfMovementStateToSlaughterhouse:
            [self pickDestinationFloorForSlaughterhouse];
            [self doMovementForMovementState:DwarfMovementStateToSlaughterhouse];
            break;
        case DwarfMovementStateToPasture:
            [self pickDestinationFloorForPasture];
            [self doMovementForMovementState:DwarfMovementStateToPasture];
            break;
        case DwarfMovementStateHunger:
            [self pickDestinationFloorForEating];
            [self doMovementForMovementState:DwarfMovementStateHunger];
            break;
        case DwarfMovementStateResting:
            [self pickDestinationFloorForResting];
            [self doMovementForMovementState:DwarfMovementStateResting];
            break;
        case DwarfMovementStateFleeingFromEnemy:
        {   
            int currentFloor = [self currentFloorByPosition];
            self.destinationFloor = currentFloor + 1; //makes the dwarf flee one floor up
            [self doMovementForMovementState:DwarfMovementStateFleeingFromEnemy];
            break;
        }

        //special case for setting up moving to a new tower
        case DwarfMovementStateToNewTower:
            [self handleMovingToNewTower];
            break;
        default:
            break;
    }
}
-(void) tryToChooseAJobWhileIdle {
    [self pickDestinationFloorForWork];

    [self calculateDestinationPositionByFloor];

    if (self.currentFloor != self.destinationFloor) {
        self.movementState = DwarfMovementStateNeedsMoving;
    } else {
        if ([self checkFloorsForJobs]) {
            self.movementState = DwarfMovementStateNeedsMoving;
        }  else {
            [self doIdleMovement];
        }
    }
}
-(void) handlePlayingMovement {
    [self pickDestinationFloorForPlaying];

    [self calculateDestinationPositionByFloor];

    if (self.currentFloor != self.destinationFloor) {
        [self doVerticalMovement];
    } else {
        [self doIdleMovement];
    }
}
-(void) handleMovingToNewTower {
    [self pickDestinationFloorForGateway];

    [self calculateDestinationPositionByFloor];

    if (![self isAtFloorDestinationPosition]) {
        [self doFloorMovement];
    } else if (![self isAtVerticalDestinationPosition]) {
        [self doVerticalMovement];
    } else {
        self.movementState = DwarfMovementStateReadyToMoveToNewTower;
    }
}
-(void) doMovementForMovementState:(DwarfMovementState)movementState {
    //the destination floor has already been chosen
    [self calculateDestinationPositionByFloor];

    if ([self doBasicMovementToFloor]) {
        switch (movementState) {
            case DwarfMovementStateToStockpile:
                self.movementState = DwarfMovementStateAtStockpile;
                break;
            case DwarfMovementStateToSlaughterhouse:
                self.movementState = DwarfMovementStateAtSlaughterhouse;
                break;
            case DwarfMovementStateToPasture:
                self.movementState = DwarfMovementStateAtPasture;
                break;
            case DwarfMovementStateHunger:
                self.movementState = DwarfMovementStateToRandomPositionForEating;
                [self pickRandomDestinationOnCurrentFloor];
                break;
            case DwarfMovementStateResting:
                self.movementState = DwarfMovementStateToRandomPositionForResting;
                [self pickRandomDestinationOnCurrentFloor];
                break;
            case DwarfMovementStateFleeingFromEnemy:
                //do nothing yet
                break;
            default:
                break;
        }
    }
}
-(BOOL) doBasicMovementToFloor {
    if (![self isAtFloorDestinationPosition]) {
        [self doFloorMovement];
    } else if (![self isAtVerticalDestinationPosition]) {
        [self doVerticalMovement];
    } else {
        [self arriveAtDestinationFloor];
        return YES;
    }
    return NO;
}

#pragma mark - Overrides
-(void) arriveAtDestinationFloor {
    //these have to be set this way to make sure movement remains aligned
    self.currentPosition = self.destinationPosition;
    self.currentFloor = self.destinationFloor;
    self.movementState = DwarfMovementStateAtDestinationFloor;
}
-(void) doFloorMovement {
    //once close enough to the destination, jump to the destination
    //all of these special cases specifically route the dwarf based on their movement state
    //once the state switches the appropriate logic will run on the next tick
    if ([self isAtFloorDestinationPosition]) {
        if (self.movementState == DwarfMovementStateToFloorExit) {
            self.currentPosition = self.destinationPosition;
            self.movementState = DwarfMovementStateAtFloorExit;
        }
        if (self.movementState == DwarfMovementStateToJobPosition) {
            self.currentPosition = self.destinationPosition;
            self.movementState = DwarfMovementStateInPositionForWork;
        }
        if (self.movementState == DwarfMovementStateToRandomPositionForEating) {
            self.currentPosition = self.destinationPosition;
            self.movementState = DwarfMovementStateInPositionForEating;
        }
        if (self.movementState == DwarfMovementStateToRandomPositionForResting) {  
            self.currentPosition = self.destinationPosition;
            self.movementState = DwarfMovementStateInPositionForResting;
        }
    } else {
        if ([self shouldMoveRight]) {
            [self moveRight];
        } else {
            [self moveLeft];
        }
    }
}

#pragma mark - Pick Destination Floors
-(void) pickDestinationFloorForWork {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorListForWork) {
        if ([floor areJobSlotsAvailable]) {
            [possibleFloorsArray addObject:floor];
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    }
}
-(void) pickDestinationFloorForEating {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList) {
        if (floor.room.roomType == Foodhall) {
            [possibleFloorsArray addObject:floor];
        }
        if (floor.room.roomType == GatewayExit) {
            [possibleFloorsArray addObject:floor];
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForResting {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList ) {
        if (floor.room.roomType == Dormitory) {
            [possibleFloorsArray addObject:floor];
        }
        if (floor.room.roomType == GatewayExit) {
            [possibleFloorsArray addObject:floor];
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForGateway {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList) {
        if (floor.room.roomType == Gateway) {
            [possibleFloorsArray addObject:floor];
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForStockpile {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList) {
        BOOL foundAStockpile = NO;
        if (floor.room.roomType == RoomTypeStockpile) {
            foundAStockpile = YES;
            [possibleFloorsArray addObject:floor];
        }
        if (!foundAStockpile) {
            if (floor.room.roomType == GatewayExit) {
                [possibleFloorsArray addObject:floor];
            }
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForSlaughterhouse {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList) {
        BOOL foundASlaughterhouse = NO;
        if (floor.room.roomType == Slaughterhouse) {
            foundASlaughterhouse = YES;
            [possibleFloorsArray addObject:floor];
        }
        if (!foundASlaughterhouse) {
            if (floor.room.roomType == GatewayExit) {
                [possibleFloorsArray addObject:floor];
            }
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForPasture {
    NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
    for (DTTowerFloor *floor in _floorList) {
        BOOL foundAPasture = NO;
        if (floor.room.roomType == RoomTypePasture) {
            foundAPasture = YES;
            [possibleFloorsArray addObject:floor];
        }
        if (!foundAPasture) {
            if (floor.room.roomType == GatewayExit) {
                [possibleFloorsArray addObject:floor];
            }
        }
    }
    if (possibleFloorsArray.count > 0) {
        self.destinationFloor = [self closestFloor:possibleFloorsArray];
    } else {
        self.destinationFloor = 0;
    }
}
-(void) pickDestinationFloorForPlaying {
    if (_floorList.count > 0) {
        int randomNumber = arc4random_uniform((int)_floorList.count);
        DTTowerFloor *floor = [_floorList objectAtIndex:randomNumber];
        self.destinationFloor = floor.floorNumber;
    } else {
        BOOL gatewayExitPresent = NO;
        for (DTTowerFloor *floor in _floorList) {
            if (floor.room.roomType == GatewayExit) {
                gatewayExitPresent = YES;
                self.destinationFloor = floor.floorNumber;
            }
        }
        if (!gatewayExitPresent) {
            self.destinationFloor = 0;
        }
    }
}

#pragma mark - Tower Transition
-(void) transitionToNewTower {
    [self calculateDestinationPositionByFloor];
    [self arriveAtDestinationFloor];
}

#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _movementState = [aDecoder decodeIntegerForKey:@"dwarfMovementState"];
        _isTiredOrHungry = [aDecoder decodeBoolForKey:@"isTiredOrHungry"];
        _floorList = [[NSMutableArray alloc]init];
        _floorListForWork = [[NSMutableArray alloc]init];
    }
    return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    [aCoder encodeInteger:self.movementState forKey:@"dwarfMovementState"];
    [aCoder encodeBool:self.isTiredOrHungry forKey:@"isTiredOrHungry"];
}

@end
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$
-(void) tryToChooseAJobWhileIdle

You don't need to say "try". I think this method name is fine as "chooseJobWhileIdle" or maybe even "lookForJob", "findJob", or maybe "idleJobSearch" perhaps. The current method name is verbose, yes, and we like that in Objective-C. We want to be descriptive and self-documenting, but not overly-so.

The method name should convey to the developer the important thing that the method does. It's not particularly important to the developer that this method may fail to find a job. And if it were, how would the developer know the dwarf failed to find a job? If we want to let the developer know that this method might fail to find a job, we can let him know by returning a BOOL indicating success/failure, and letting him potentially act on that knowledge.


-(void) pickDestinationFloorForEating

You have a handful of methods like this, and the logic is the same in all of them. The logic should be combined into a single method. I'm fining keeping these methods here as convenience methods, but the actual logic should exist in a single method. This may mean refactoring the actual logic some, but in the end, it will be better when you go back to adding more of these sorts of methods.


if (currentFloor > floor.floorNumber) {
    for (int i = currentFloor; i > floor.floorNumber; i--) {
        currentCount++;
    }
} else {
    for (int i = currentFloor; i < floor.floorNumber; i++) {
        currentCount++;
    }
}

Not that loops within loops can always be avoided, but they should be a bit of a flag to stop and look to make sure they can't be avoided. A loop is an effective way to get a small block of code to execute several times... so a loop in a loop is a way of getting a loop (which will execute its body several times each time) to execute several times. You can spend a lot of time executing nested loops.

Here, it's unnecessary. All of that code is replaced with this:

currentCount += abs(currentFloor - floor.floorNumber);

You remove the time it took to check the if conditional each run through the outer loop, the time it took to check the for conditional, and increment/decrement two variables each time through the inner loop. You also completely remove the loop iteration variable.

And honestly, in that method I think this is the only place currentCount is modified, so we can get rid of resetting it to 0 at the end of each loop, and simple do this:

currentCount = abs(currentFloor - floor.floorNumber);

Which is probably slightly more efficient than the += and certainly saves the time resetting currentCount to 0 on each iteration.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for another great review! I really appreciate the tips about abs() because I could not think of a shorter way to calculate distance outside of counting the loops. The abs() way is so much cleaner. \$\endgroup\$
    – bazola
    Jul 23, 2014 at 21:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.