So I've been working on AI for a Tower Building simulation game for quite a few days, and I think the code would really benefit from review. I'm a hobbyist programmer, but I really care about doing things properly so don't be afraid to be harsh. I want to learn.
The basic idea of the job management system is as follows. The user chooses a job with touch commands in the scene. The game evaluates if the job is valid, and if there are enough resources to perform the job. If so, it passes the job to the tower, which passes the job to the appropriate tower floor object.
The dwarves are held in an array by the tower. Each tick, it passes each dwarf an array of floors that contain jobs. The AI of the dwarf chooses which floor to travel to based on its current state, whether all the job slots are full on the floor, and it always moves to the closest floor with available jobs.
When it gets to the floor, the tower passes the dwarf to the floor object. The floor object then tells the dwarf where the position of the job is, and fills one of the active job slots until there are none remaining. When the dwarf reaches the position of the job, the floor sets it to working, and collects the completed job when it is finished. It then tells the dwarf to move to the exit of the floor. The dwarves only move left, right, up and down.
Here is the code.
First the character object, which the dwarves inherit from.
DTCharacter.h:
#import <Foundation/Foundation.h>
@interface DTCharacter : NSObject
//need the world size for proper positioning
@property CGSize worldSize;
@property CGPoint currentPosition;
@property CGPoint destinationPosition;
@property int currentFloor;
@property int destinationFloor;
-(void) moveUp;
-(void) moveDown;
-(void) moveLeft;
-(void) moveRight;
//only used by animals now
@property BOOL isIdle;
@end
DTCharacter.m:
#import "DTCharacter.h"
@implementation DTCharacter
#pragma mark - Movement
-(void) moveUp {
self.currentPosition = CGPointMake(0, self.currentPosition.y + self.worldSize.height / 18);
}
-(void) moveDown {
self.currentPosition = CGPointMake(0, self.currentPosition.y - self.worldSize.height / 18);
}
-(void) moveLeft {
self.currentPosition = CGPointMake(self.currentPosition.x - self.worldSize.width / 18, self.currentPosition.y);
}
-(void) moveRight {
self.currentPosition = CGPointMake(self.currentPosition.x + self.worldSize.width / 18, self.currentPosition.y);
}
@end
The state enum for dwarves:
typedef NS_ENUM(NSInteger, DwarfStatus){
Idle = 0,
NeedsMoving,
MovingToFloor,
AtDestinationFloor,
CarryingItemToStockpile,
Hungry,
Eating,
MovingToJobPosition,
MovingToFloorExit,
InPositionForWork,
Working,
FinishedWorking,
AtFloorExit,
Dead
};
DTDwarf.h:
#import <Foundation/Foundation.h>
#import "DTCharacter.h"
#import "DTDwarfStatus.h"
#import "DTJobTypes.h"
@interface DTDwarf : DTCharacter
//update loop
-(void) checkStatus;
@property DwarfStatus status;
//The tower gives each dwarf this list of floors each tick
@property NSMutableArray *availableFloorsArray;
@property CGPoint floorDestination; //used for horizontal movement
@property JobType jobType;
@end
DTDwarf.m:
#import "DTDwarf.h"
#import "DTJob.h"
#import "DTTowerFloor.h"
@implementation DTDwarf {
int _miningDelayCount;
int _currentMiningCount;
int _ladderBuildDelayCount;
int _currentLadderCount;
int _buildDelayCount;
int _currentBuildCount;
int _roomUpgradeDelayCount;
int _currentRoomUpgradeCount;
}
-(id) init {
self = [super init];
if (self) {
_status = Idle;
_jobType = NoCurrentJob;
//default stats for dwarf
_miningDelayCount = 8;
_currentMiningCount = _miningDelayCount;
_ladderBuildDelayCount = 8;
_currentLadderCount = _ladderBuildDelayCount;
_buildDelayCount = 8;
_currentBuildCount = _buildDelayCount;
_roomUpgradeDelayCount = 20;
_currentRoomUpgradeCount = _roomUpgradeDelayCount;
_availableFloorsArray = [[NSMutableArray alloc]init];
}
return self;
}
#pragma mark - Update Loop
-(void) checkStatus {
[self pickDestinationFloorForWork];
switch (self.status) {
//rest and work states
case Idle:
if ([self checkFloorsForJobs]) {
self.status = NeedsMoving;
} else {
[self doIdleMovement];
}
break;
case AtFloorExit:
self.status = MovingToFloor;
break;
case AtDestinationFloor:
if (![self checkFloorsForJobs]) {
self.status = MovingToFloorExit;
}
break;
case Working:
if ([self doJobCountdown]) {
self.status = FinishedWorking;
}
break;
//Movement states
case NeedsMoving:
self.status = MovingToFloorExit;
break;
case MovingToFloorExit:
[self doFloorMovement];
break;
case MovingToFloor:
[self doVerticalMovement];
break;
case MovingToJobPosition:
[self doFloorMovement];
break;
default:
break;
}
[self sayStatus];
}
-(void) sayStatus {
//overriden by subclass
}
-(void) pickDestinationFloorForWork {
NSMutableArray *floorArray = self.availableFloorsArray;
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in floorArray) {
if ([floor areJobSlotsAvailable]) {
[possibleFloorsArray addObject:floor];
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self findClosestFloor:possibleFloorsArray];
}
}
-(BOOL) checkFloorsForJobs {
NSMutableArray *floorArray = self.availableFloorsArray;
for (DTTowerFloor *floor in floorArray) {
if ([floor areJobSlotsAvailable]) {
return YES;
}
}
return NO;
}
-(int) findClosestFloor:(NSMutableArray *)possibleFloors {
int currentFloor = [self calculateCurrentFloorByPosition];
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 lowest count
if (currentCount < bestCount) {
destinationFloor = floor.floorNumber;
bestCount = currentCount;
currentCount = 0;
}
}
return destinationFloor;
}
-(int) calculateCurrentFloorByPosition {
int floorSize = self.worldSize.height/6;
return self.currentPosition.y / floorSize;
}
#pragma mark - Job Countdowns
-(BOOL) doJobCountdown {
BOOL didFinishJobCountdown = NO;
switch (self.jobType) {
case MiningJob:
if ([self miningJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
case LadderJob:
if ([self ladderJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
case BottomBuildJob:
if ([self buildJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
case WallBuildJob:
if ([self buildJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
case RoomBuildJob:
if ([self buildJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
case RoomUpgradeJob:
if ([self roomUpgradeJobCountdown]) {
[self resetCountdowns];
didFinishJobCountdown = YES;
}
break;
default:
break;
}
return didFinishJobCountdown;
}
-(BOOL) miningJobCountdown {
if (_currentMiningCount > 0) {
_currentMiningCount--;
return NO;
} else {
return YES;
}
}
-(BOOL) ladderJobCountdown {
if (_currentLadderCount > 0) {
_currentLadderCount--;
return NO;
} else {
return YES;
}
}
-(BOOL) buildJobCountdown {
if (_currentBuildCount > 0) {
_currentBuildCount--;
return NO;
} else {
return YES;
}
}
-(BOOL) roomUpgradeJobCountdown {
if (_currentRoomUpgradeCount > 0) {
_currentRoomUpgradeCount--;
return NO;
} else {
return YES;
}
}
-(void) resetCountdowns {
_currentMiningCount = _miningDelayCount;
_currentLadderCount = _ladderBuildDelayCount;
_currentBuildCount = _buildDelayCount;
_currentRoomUpgradeCount = _roomUpgradeDelayCount;
}
#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];
}
}
}
-(void) calculateDestinationPositionByFloor {
int floorSize = self.worldSize.height/6;
int groundHeightOffset = self.worldSize.height/6/3;
self.destinationPosition = CGPointMake(0, self.destinationFloor * floorSize - groundHeightOffset);
}
-(BOOL) isAtVerticalDestinationPosition {
BOOL isAtDestinationPosition = NO;
int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
int minimumDistance = self.worldSize.height/18;
if (distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance) {
isAtDestinationPosition = NO;
} else {
isAtDestinationPosition = YES;
}
return isAtDestinationPosition;
}
-(void) arriveAtDestinationFloor {
self.currentPosition = self.destinationPosition;
self.currentFloor = self.destinationFloor;
self.status = AtDestinationFloor;
}
-(BOOL) shouldMoveUp {
int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
if (distanceFromDestination < 0) {
return YES;
} else {
return NO;
}
}
#pragma mark - Horizontal Movement
-(void) doFloorMovement {
//once close enough to the destination, jump to the destination
if ([self isAtFloorDestinationPosition]) {
if (self.status == MovingToFloorExit) {
self.currentPosition = self.destinationPosition;
self.status = AtFloorExit;
}
if (self.status == MovingToJobPosition) {
self.currentPosition = self.destinationPosition;
self.status = InPositionForWork;
}
} else {
if ([self shouldMoveRight]) {
[self moveRight];
} else {
[self moveLeft];
}
}
}
-(BOOL) isAtFloorDestinationPosition {
BOOL isAtDestinationPosition = NO;
int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
int minimumDistance = self.worldSize.width/18;
if (distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance) {
isAtDestinationPosition = NO;
} else {
isAtDestinationPosition = YES;
}
return isAtDestinationPosition;
}
-(BOOL) shouldMoveRight {
int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
if (distanceFromDestination < 0) {
return YES;
} else {
return NO;
}
}
#pragma mark - Idle Movement
-(void) doIdleMovement {
int lowerBoundX = -25;
int randomHigherBoundX = arc4random_uniform(51);
int randomPosition = lowerBoundX + randomHigherBoundX;
CGPoint tempPosition = CGPointMake(self.currentPosition.x + randomPosition, self.currentPosition.y);
if (tempPosition.x < -self.worldSize.width/2) {
tempPosition.x = -self.worldSize.width/2 + 50;
}
if (tempPosition.x > self.worldSize.width/2){
tempPosition.x = self.worldSize.width/2 - 50;
}
self.currentPosition = tempPosition;
}
@end
DTTowerFloor.h:
#import <Foundation/Foundation.h>
#import "DTGroundTypes.h"
#import "DTRoom.h"
#import "DTJob.h"
#import "DTDwarf.h"
@interface DTTowerFloor : NSObject
-(id) initWithType:(GroundType)groundType andFloor:(int)floorNumber andWorldSize:(CGSize)worldSize;
@property int floorNumber;
@property GroundType groundType;
@property int backgroundType;
//randomly determined at the start
@property int blocksLeftCount;
@property int startingBlockCount;
//update loop
-(void) updateFloor;
//these are built by the workers
//used in the rules to determine if certain buildings are allowed to be built
@property BOOL hasWalls;
@property BOOL hasBottom;
@property BOOL hasLadder;
@property BOOL hasRoom;
@property BOOL hasRoomUpgrade;
@property DTRoom *room;
@property RoomType roomType;
//for rendering
@property BOOL isRevealed;
@property BOOL isBackgroundRevealed;
//these are for the room upgrade jobs
@property int numWorkSpacesLeft;
@property int numWorkers;
//dwarf management
-(void) acceptDwarf:(DTDwarf *)dwarf;
@property NSMutableArray *dwarfArray;
@property NSMutableArray *dwarvesForPickup;
//job management
-(BOOL) areJobSlotsAvailable;
-(void) buildRoomUpgrade:(RoomType)roomType;
-(void) addJob: (DTJob *)job;
@property NSMutableArray *jobArray;
@property NSMutableArray *completedJobsForPickup;
@end
DTTowerFloor.m:
#import "DTTowerFloor.h"
@implementation DTTowerFloor {
CGPoint _floorCenterPosition;
CGSize _worldSize;
DTJob *_activeJob;
NSMutableArray *_pendingJobUnits;
NSMutableArray *_availableJobSlots;
NSMutableArray *_completedJobUnits;
int _totalJobSlots;
int _remainingJobSlots;
}
#pragma mark - Initialization
-(id) initWithType:(GroundType)groundType andFloor:(int)floorNumber andWorldSize:(CGSize)worldSize {
self = [super init];
if (self) {
_groundType = groundType;
_floorNumber = floorNumber;
[self setBackgroundForGroundType];
//for routing dwarf movement
_worldSize = worldSize;
_floorCenterPosition = CGPointMake(0, _floorNumber * _worldSize.height/6 - _worldSize.height/6/3);
//number of blocks that will need to be mined out
_startingBlockCount = [self setInitialBlockCount];
_blocksLeftCount = _startingBlockCount;
//number of workers that can work at once
_totalJobSlots = 4;
_remainingJobSlots = _totalJobSlots;
_jobArray = [[NSMutableArray alloc]init];
_pendingJobUnits = [[NSMutableArray alloc]init];
_availableJobSlots = [[NSMutableArray alloc]init];
_completedJobUnits = [[NSMutableArray alloc]init];
_completedJobsForPickup = [[NSMutableArray alloc]init];
_dwarfArray = [[NSMutableArray alloc]init];
_dwarvesForPickup = [[NSMutableArray alloc]init];
}
return self;
}
-(void) setBackgroundForGroundType {
//eventually this will determine the background based on the type and depth
}
-(int) setInitialBlockCount {
int randomNumber = arc4random_uniform(7) + 3;
return randomNumber;
}
#pragma mark - Update loop
-(void) updateFloor {
//later these would be controlled and activated or disabled when the floor is in different states
[self chooseAnActiveJob];
[self startActiveJob];
[self fillJobSlotsWithJobs];
[self setPositionsForJobs];
[self updateFloorDwarves];
[self putDwarvesToWork];
[self checkDwarvesForWork];
[self countFinishedJobs];
[self putDwarvesInExitQueue];
}
-(void) chooseAnActiveJob {
if (self.jobArray.count > 0 && _activeJob == nil) {
_activeJob = [self.jobArray firstObject];
[self.jobArray removeObjectAtIndex:0];
}
}
-(void) startActiveJob {
if (_activeJob != nil && _activeJob.jobsToCreate > 0) {
int jobCount = 0;
if (_activeJob.jobType == MiningJob) {
jobCount = _blocksLeftCount;
_activeJob.jobUnitsNeeded = jobCount;
} else {
jobCount = _activeJob.jobsToCreate;
}
for (int i = 0; i < jobCount; i++) {
//make copies of the job to expand into the pending jobs array
DTJob *jobCopy = [[DTJob alloc]initWithType:_activeJob.jobType];
jobCopy.status = InQueue;
[_pendingJobUnits addObject:jobCopy];
}
_activeJob.jobsToCreate = 0;
}
}
-(void) fillJobSlotsWithJobs {
if (_availableJobSlots.count < _totalJobSlots) {
int spotsToFill = _totalJobSlots - _availableJobSlots.count;
for (int i = 0; i < spotsToFill ; i++) {
if (_pendingJobUnits.count > 0) {
[_availableJobSlots addObject:[_pendingJobUnits lastObject]];
[_pendingJobUnits removeLastObject];
}
}
}
}
-(void) setPositionsForJobs {
for (DTJob *job in _availableJobSlots) {
if (job.status == InQueue) {
// put the job at a semi random x position and at the floor level
int randomNumber = arc4random_uniform(4) + 1;
job.jobPosition = CGPointMake((_worldSize.width/3)/randomNumber, _floorCenterPosition.y);
job.status = WaitingForWorker;
}
}
}
-(void) updateFloorDwarves {
for (DTDwarf *dwarf in self.dwarfArray) {
//this is the dwarf update loop
[dwarf checkStatus];
}
}
-(void) putDwarvesToWork {
for (DTDwarf *dwarf in self.dwarfArray) {
if (dwarf.status == AtDestinationFloor || dwarf.status == AtFloorExit) {
if ([self areJobSlotsAvailable] && _remainingJobSlots > 0) {
[self assignDwarfMovementForJob:dwarf];
} else {
dwarf.status = Idle;
}
}
}
}
-(void) assignDwarfMovementForJob:(DTDwarf *)dwarf {
//assign one job at once out of the availableJobSlots array until it is full
int jobsToAssign = 1;
int numJobsAssigned = 0;
for (DTJob *tempJob in _availableJobSlots) {
if (tempJob.status == WaitingForWorker && numJobsAssigned < jobsToAssign) {
if (tempJob.jobType == MiningJob) {
//if its a mining job, place it at the position of the last remaining block out of 10 possible
int lastBlockNumber = _blocksLeftCount;
dwarf.destinationPosition = CGPointMake(-_worldSize.width/2.22 + lastBlockNumber * _worldSize.width/10, _floorCenterPosition.y);
} else {
//otherwise it was already calculated and set in another method
dwarf.destinationPosition = tempJob.jobPosition;
}
dwarf.status = MovingToJobPosition;
tempJob.status = WorkerIsWorking;
numJobsAssigned++;
_remainingJobSlots--;
}
}
}
-(void) checkDwarvesForWork {
for (DTDwarf *dwarf in self.dwarfArray) {
if (dwarf.status == InPositionForWork) {
[self startDwarfWorking:dwarf];
}
if (dwarf.status == FinishedWorking) {
if (_activeJob.jobType == MiningJob) {
self.blocksLeftCount--;
}
_remainingJobSlots++;
[self moveDwarfToExit:dwarf];
[self finishOneJobUnit];
}
}
}
-(void) countFinishedJobs {
if (_activeJob != nil) {
if (_completedJobUnits.count >= _activeJob.jobUnitsNeeded) {
_activeJob.status = AllUnitsCompleted;
[self.completedJobsForPickup addObject:_activeJob];
_activeJob = nil;
[self clearAllJobUnits];
}
}
}
-(void) clearAllJobUnits {
[_availableJobSlots removeAllObjects];
[_completedJobUnits removeAllObjects];
}
-(void) putDwarvesInExitQueue {
NSMutableArray *dwarvesStaying = [[NSMutableArray alloc]init];
for (DTDwarf *dwarf in self.dwarfArray) {
if (dwarf.status == AtFloorExit || dwarf.status == Idle) {
if (![self areJobSlotsAvailable]) {
[self.dwarvesForPickup addObject:dwarf];
} else {
[dwarvesStaying addObject:dwarf];
}
} else {
[dwarvesStaying addObject:dwarf];
}
}
self.dwarfArray = dwarvesStaying;
}
#pragma mark - Job Handling
-(void) addJob: (DTJob *)job {
job.status = InQueue;
job.floorNumber = self.floorNumber;
[self.jobArray addObject:job];
}
-(BOOL) areJobSlotsAvailable {
int jobsWaitingForWorkers = 0;
for (DTJob *job in _availableJobSlots) {
if (job.status == WaitingForWorker) {
jobsWaitingForWorkers++;
}
}
if (_remainingJobSlots > 0 && jobsWaitingForWorkers > 0) {
return YES;
} else {
return NO;
}
}
-(void) finishOneJobUnit {
if (_availableJobSlots.count > 0) {
DTJob *job = [_availableJobSlots firstObject];
job.status = UnitCompleted;
[_completedJobUnits addObject:job];
[_availableJobSlots removeObjectAtIndex:0];
}
}
#pragma mark - Dwarf Handling
-(void) acceptDwarf:(DTDwarf *)dwarf {
[self.dwarfArray addObject:dwarf];
}
-(void) startDwarfWorking:(DTDwarf *)dwarf {
dwarf.status = Working;
dwarf.jobType = _activeJob.jobType;
}
-(void) moveDwarfToExit:(DTDwarf *)dwarf {
dwarf.status = MovingToFloorExit;
dwarf.destinationPosition = _floorCenterPosition;
}
#pragma mark - Rooms
//rooms are paid for up front and built instantly, there are many types
-(void) buildRoomUpgrade:(RoomType)roomType {
self.roomType = roomType;
self.hasRoomUpgrade = YES;
self.room = [[DTRoom alloc]initWithRoomType:roomType];
}