Tile Engine: The Ai

dumb and dumber

For the Ai I have two classes. One which is the Tank class for the Ai, and the other which is the brain of the Ai, or rather how prone is a particular tank to make certain decisions. The brain could have been an Internal class, since it is only instantiated by the Ai.

First I'll cover the AiTank class.

The main differences here are related to all the things the user can do with the player tank that the Ai must learn to do by itself. So shooting is different, and movement is different as related to strategy of movement as well as how to get unstuck.

The AiTank is placed randomly at the top of the maze, and it is set at a randomly picked direction. As it moves it can randomly turn and randomly shoot. If this is a clever Tank when it chooses to turn, it will prefer to turn downwards, towards the player's HQ, more than in any other direction. If this is a dumb tank, it will pick to turn upwards, or sideways. In fact let me go over between the differences of dumb and clever.

Clever Tank: tends to turn downwards, tends to shoot more, tends to blink more often (becoming invulnerable.)

Dumb Tank: tends to turn randomly and more erratically, shoots less and blinks less.

Plus, if the PlayerTank is in sight (line of fire) this increases the AiTank's probability of shooting at the player.

The Ai obeys a series of Timer objects, which are used so that the tank will not shoot too often, or turn too often, or unstuck the moment it gets stuck... This serves to break down the feeling of synchronized movement between the various enemy tanks.

Controlling Movement

When the AiTank is stuck, it checks to see which direction is currently blocked to it, and then picks one of the remaining directions. It will run the same logic as well when and if it decides to turn randomly.

This choice of where to turn to is picked from a pattern of choices; an Array of values which is shuffled constantly but it is still mathematically more probable to return the number 1, and then the number 2 and finally (less probable) to return the number 3. This is a very good way of building Ais that follow strict strategies, so that even if the decision of which strategy to follow is made randomly, you can still make sure that some decisions are more probable than others.

You do this by storing the KEY_INDEX of each decision in an array. So in this case I have three options, and they go from the more probable to the less probable to occur. So I store them in an array like this:
[1,1,1,1,2,2,2,3,3]

By shufffling the array and randomly picking an index, I can increase the possibility of certain KEY_INDEXES being picked, and still use Math.random only once.

Like I said before, this tutorial is about Tile collision, so I won't go over everything in these classes. But I trust my comments may help understanding them.

The AiTank class

package tiletank.tank {
	
	import flash.utils.Timer;
	import flash.events.TimerEvent;
	import flash.events.Event;

	import tiletank.*;
	
	
	/**
	* The basic class for enemy tanks
	*/
	public class AiTank extends BasicTank {
		
		private const ALPHA:Number = .9;
		//this will take care of movement decisions
		private var _brain:AiBrain;
		
		private var _directions:Array = [GameConstants.DOWN, GameConstants.LEFT, GameConstants.RIGHT, GameConstants.UP];
		
		private var _waitDown:Timer;
		private var _waitRandom:Timer;
		private var _waitUnstuck:Timer;
		private var _waitShoot:Timer;

		private var _blinker:Timer;
		private var _oldX:int;
		private var _oldY:int;
		
		private var _player:PlayerTank;
		
		private var _blinking:Boolean = false;
		
		function AiTank () {
			super();
			
			//create strategy object for this tank (pass boolean for SMART TANK or DUMB TANK)
			_brain = new AiBrain(_gameData.enemiesCreated % 2 == 0);
			
			//initial direction of the tank
			scaleX = _brain.initState.x;
			scaleY = _brain.initState.y;
			
			states.gotoAndStop(_brain.initState.frame+"_move");
			
			GameUtils.setColor(this, AiBrain.aiColor);
			alpha = ALPHA;
			
			_player = StageElements.instance.getElementByClass(GameConstants.PLAYER_TANK) as PlayerTank;
			
			initTimers();
		}
		public function get blinking ():Boolean {
			return _blinking;
		}
		
		/**
		* The AI update checks on the current set direction of the tank
		*/
		override public function update ():void {
			super.update();
			
			switch (getDirection()) {
				case GameConstants.UP:
					_vy = -_speed;
				break;
				case GameConstants.DOWN:
					_vy = _speed;
				break;
				case GameConstants.LEFT:
					_vx = -_speed;
				break;
				case GameConstants.RIGHT:
					_vx = _speed;
				break;
			}
			
			_nextX = _nowX + _vx;
			_nextY = _nowY + _vy;
			
			shoot();
		}
		
		/**
		* AI move logic
		*/
		override public function move ():void {
			
			//if position has not changed since last iteration
			if (_oldX == _nextX && _oldY == _nextY) {
				//ai is stuck
				
				//if currently not waiting to run UNSTUCK logic...
				if (!_waitUnstuck.running) {
					
					if (getDirection() == GameConstants.UP || getDirection() == GameConstants.DOWN) {
						unstuckUpDown();
					} else {
						unstuckLeftRight();
					}
					
					//change time to run unstuck logic (so it's not always the same, looks better, less synchronized)
					_waitUnstuck.delay = Math.round(Math.random()*_brain.aiWaitToUnstuck) + 1;
					_waitUnstuck.reset();
					_waitUnstuck.start();
				}
			} else {
				//tank is not stuck
				
				_nowX = _oldX = _nextX;
				_nowY = _oldY = _nextY;
				
				//tank may turn randomly
				if (!_waitRandom.running) {
					turnRandomly();
					_waitRandom.reset();
					_waitRandom.start();
				}
				
				//tank may turn towards the bottom of the maze (closer to the player's HQ)
				if (!_waitDown.running) {
					if (canGoDown()) {
						setNewDirection(GameConstants.DOWN);
					}
					_waitDown.reset();
					_waitDown.start();
				}
			}
			
			render();
		}
		
		override public function destroy ():void {
			_gameStage.removeAi(this);
		}
		
		private function shoot ():void {
			
			//if currently waiting to shoot, simply return
			if (_waitShoot.running) {
				return;
			}
			
			if (_bullet) return;//only one shot at a time, on stage
			
			var inSight:Boolean = playerInSight();
			
			//now check if player is not in sight
			if (!inSight) {
				//shoot or don't, based on shooting probability
				if (Math.random() > _brain.shootRatio) return;
			}
			
			//now create shot
			_bullet = _gameStage.addAiBullet(this);
			//decide if tank will blink (tank becomes invulnerable while blinking)
			if ( GameUtils.getRandom10() < _brain.blinkRatio) startBlinking();
			
			//start timer for next shot
			_waitShoot.reset();
			_waitShoot.start();
		}
		
		/**
		* tank is stuck while trying to move either UP or DOWN
		* The option relates to a decision, each number forces a specific decision of the AI
		*
		*/
		private function unstuckUpDown (option:int = 0):void {
			if (option == 0) option = _brain.aiMoveOption;
			
			//if ai is at the bottom, make it turn to the base
			if (_nowY > _gameStage.height - this.height) {
				if (GameUtils.getRandom10() < 3) {
					setNewDirection(_brain.aiNextXDir);
				} else {
					setNewDirection(GameConstants.RIGHT);
				}
				return;
			}
			
			switch (option) {
				case 1:
					setNewDirection(_brain.aiNextXDir);
				break;
				case 2:
					setNewDirection(_brain.aiNextXDir);
				break;
				case 3:
					//go opposite
					if (getDirection() == GameConstants.UP) {
						setNewDirection(GameConstants.DOWN);
					} else {
						setNewDirection(GameConstants.UP);
					}
				break;
				
			}
		}
		
		/**
		* Tank is stuck while moving either LEFT or RIGHT
		*/
		private function unstuckLeftRight (option:int = 0):void {
			
			if (option == 0) option = _brain.aiMoveOption;
			
			switch (option) {
				case 1:
					//more to top go down
					if (_nowY < stage.stageHeight/2) {
						setNewDirection(GameConstants.DOWN);
					} else {
					//else, pick 2 or 3
						if (GameUtils.getRandom() > 5) {
							unstuckLeftRight (2);
						} else {
							unstuckLeftRight (3);
						}
					}
				break;
				case 2:
					//go opposite
					if (getDirection() == GameConstants.LEFT) {
						setNewDirection(GameConstants.RIGHT);
					} else {
						setNewDirection(GameConstants.LEFT);
					}
				break;
				case 3:
					setNewDirection(_brain.aiNextYDir);
				break;
				
			}
		}
		
		/**
		* Tank may turn to a random direction every now and then
		*/
		private function turnRandomly ():void {
			
			//pick random: unstuckUpDown or unstuckLeftRight or do nothing
			
			if (Math.random() > _brain.turnRatio) return;
			if (GameUtils.getRandom10() >= 5) {
				unstuckUpDown();
			} else {
				unstuckLeftRight();
			}
		}
		
		/**
		* Check if DOWN movement is possible (no walls on the way)
		*/
		private function canGoDown ():Boolean {
			if (Math.random() > _brain.downRatio) return false;
			var result:int = getPointDown();
			if (result == -1) return true;
			return false;
		}
		
		/**
		* check if player tank is in sight
		*/
		private function playerInSight ():Boolean {
			switch (getDirection()) {
				case GameConstants.UP:
					if (_player.nowY < _nowY && Math.abs(_player.nowX - _nowX) <= this.width/2) {
						return true;
					}
				break;
				case GameConstants.DOWN:
					if (_player.nowY > _nowY && Math.abs(_player.nowX - _nowX) <= this.width/2) {
						return true;
					}
				break;
				case GameConstants.LEFT:
					if (_player.nowX < _nowX && Math.abs(_player.nowY - _nowY) <= this.height/2) {
						return true;
					}
				break;
				case GameConstants.RIGHT:
					if (_player.nowX > _nowX && Math.abs(_player.nowY - _nowY) <= this.height/2) {
						return true;
					}
				break;
			}
			
			return false;
		}
		
		/**
		* New direction has been chosen, update tank sprite
		*/
		private function setNewDirection (dir:String):void {
			switch (dir) {
				case GameConstants.UP:
					scaleY = 1;
					if (states.currentLabel != GameConstants.UP+"_move" ) states.gotoAndStop(GameConstants.UP+"_move");
					
				break;
				case GameConstants.DOWN:
					scaleY = -1;
					if (states.currentLabel != GameConstants.UP+"_move" ) states.gotoAndStop(GameConstants.UP+"_move");
					
				break;
				case GameConstants.LEFT:
					scaleX = -1;
					if (states.currentLabel != GameConstants.SIDE+"_move" ) states.gotoAndStop(GameConstants.SIDE+"_move");
					
				break;
				case GameConstants.RIGHT:
					scaleX = 1;
					if (states.currentLabel != GameConstants.SIDE+"_move" ) states.gotoAndStop(GameConstants.SIDE+"_move");
					
				break;
			}
		}
		
		
		override protected function initMe (event:Event):void {
			super.initMe(event);
			//default speed of the AI tank
			_speed = 2;
			_blinker = new Timer (50,40);
			_blinker.addEventListener(TimerEvent.TIMER, blink);
			_blinker.addEventListener(TimerEvent.TIMER_COMPLETE, unblink);
			
			_oldX = _nowX;
			_oldY = _nowY;
			
			//by default AI tank blinks as it first appears
			startBlinking();
		}
		private function startBlinking ():void {
			if (_blinking) return;
			_blinking = true;
			_blinker.reset();
			_blinker.start();
		}
		private function blink (event:TimerEvent):void {
			if (_gameData.gameMode == GameConstants.PAUSE) return;
			alpha = alpha > 0 ? alpha = 0 : alpha = ALPHA;
		}
		private function unblink (event:TimerEvent):void {
			_blinking = false;
			alpha = 1;
		}
		
		private function initTimers ():void {
			_waitDown = new Timer(_brain.aiWaitToGoDown, 1);
			_waitRandom = new Timer(_brain.aiWaitRandomMove, 1);
			_waitUnstuck = new Timer(Math.round(Math.random()*_brain.aiWaitToUnstuck) + 1, 1);
			_waitShoot = new Timer(_brain.aiWaitToShoot, 1);
			_waitDown.start();
			_waitRandom.start();
			_waitUnstuck.start();
			_waitShoot.start();
			
		}
	}
	
}

The AiBrain class

package tiletank.tank {
	
	import tiletank.GameData;
	import tiletank.GameConstants;
	import tiletank.GameUtils;
	
	/**
	* Here the IQ of the Tank is decided
	*/
	public class AiBrain  {
		
		
		/**
		* One Important element of this class are its statis properties.
		* In order to reduce the feeling of synchronism in the movements of the various tanks,
		* I always store the last direction of movement picked by a tank (any tank)
		* and so when the next tank needs to pick a direction, it will try to pick a different one
		* than the previous
		*/
		private static var _aiNextXDir:String;
		private static var _aiNextYDir:String;
		
		private static var _aiIndex:int = 0;
		private static var _gameData:GameData;
		
		
		// the scale values and original direction for when the tank first appears
		private var _aiInitStates:Array =[{y:-1, x:1, frame:GameConstants.UP},
										  {y:-1, x:1, frame:GameConstants.SIDE},
										  {y:-1, x:-1, frame:GameConstants.UP},
										  {y:-1, x:-1, frame:GameConstants.SIDE}];
		//the patterns of movements (a greater chance to pick 1, a smaller chance of picking 2, and an even smaller chance of picking 3								
		private var _aiMoveOptions:Array= [1,1,1,1,2,2,2,3,3];
		//the values for the timers
		private var _aiWaitRandomMove:int;
		private var _aiWaitToShoot:int;
		private var _aiWaitToUnstuck:int;
		
		private var _smart:Boolean;
		
		//values for how often the tank will choose to perform the following actions
		public var downRatio:Number;
		public var turnRatio:Number;
		public var shootRatio:Number;
		public var blinkRatio:Number;
		
		function AiBrain (smart:Boolean) {
			if (!_gameData) _gameData = GameData.instance;
			_smart = smart;
			setTimerDelays();
			setCleverness();
		}
		
		/**
		* retrieve information on initial appearance of Tank, based on this tank's index
		*/
		public function get initState ():Object {
			var indx:int = _aiIndex;
			_aiIndex++;
			if (_aiIndex == _aiInitStates.length) _aiIndex = 0;
			return _aiInitStates[indx];
		}
		
		public static function get aiColor ():uint {
			return _gameData.maze.foreground;
		}
		
		/**
		* The patterns of movement array is shuffled and a random value is returned (either 1, 2 or 3)
		*/
		public function get aiMoveOption ():int {
			_aiMoveOptions = GameUtils.shuffleArray(_aiMoveOptions.concat());
			return _aiMoveOptions[Math.floor(Math.random()*_aiMoveOptions.length)];
		}
		
		public function get aiWaitToGoDown ():int {
			return _gameData.maze.gapRatio*1000;
		}
		public function get aiWaitRandomMove ():int {
			return _aiWaitRandomMove;
		}
		public function get aiWaitToShoot ():int {
			return _aiWaitToShoot;
		}
		public function get aiWaitToUnstuck ():int {
			return _aiWaitToUnstuck;
		}
		/**
		* Return the next X direction as the opposite of the last X direction chosen by a tank
		*/
		public function get aiNextXDir ():String {
			if (_aiNextXDir == GameConstants.LEFT) {
				_aiNextXDir = GameConstants.RIGHT;
			} else {
				_aiNextXDir = GameConstants.LEFT;
			}
			return _aiNextXDir;
		}
		/**
		* Return the next Y direction as the opposite of the last Y direction chosen by a tank
		*/
		public function get aiNextYDir ():String {
			if (_aiNextYDir == GameConstants.DOWN) {
				_aiNextYDir = GameConstants.UP;
			} else {
				_aiNextYDir = GameConstants.DOWN;
			}
			return _aiNextYDir;
		}
		
		
		private function setTimerDelays ():void {
			//init values
			_aiWaitRandomMove = 4000;
			_aiWaitToShoot = 5000;
			_aiWaitToUnstuck = 1000;
			
			if (_smart) {
				//shoot more
				_aiWaitToShoot -= _gameData.level*50;
				//unstuck faster
				_aiWaitToUnstuck -= _gameData.level*20;
				//move less randomly
				_aiWaitRandomMove += _gameData.level*50;
			} else {
				//shoot less
				_aiWaitToShoot -= _gameData.level*5;
				//take longer to unstuck
				_aiWaitToUnstuck -= _gameData.level*5;
				//move more randomly
				_aiWaitRandomMove -= _gameData.level*50;
			}
			//set limits
			if (_aiWaitToShoot < 500) _aiWaitToShoot = 500;
			if (_aiWaitToUnstuck < 200) _aiWaitToShoot = 200;
			if (_aiWaitRandomMove < 800) _aiWaitRandomMove = 800;
		}
		private function setCleverness ():void {
			
			
			if (_smart){
				//move down more times
				downRatio = 0.6;
				//turn less
				turnRatio = 0.4;
				//shoot more
				shootRatio = 0.6;
				//blink more (become invulnerable)
				blinkRatio = 7;
			} else {
				//move down less
				downRatio = 0.1;
				//turn more
				turnRatio = 0.8;
				//shoot less
				shootRatio = 0.1;
				//blink less (become invulnerable)
				blinkRatio = 2;
				
			}
		}

	}
	
}