Beispiel #1
0
        public TankAction[] GetTankActionsFromTankToAttackTankAtPointAlongDirectionOfMovement(
            int playerIndex, int tankNumber, Point targetPoint, Direction finalMovementDir,
            EdgeOffset[] edgeOffsets, bool keepMovingCloserOnFiringLastBullet)
        {
            Tank                           tank                      = Game.Current.Players[playerIndex].Tanks[tankNumber];
            MobileState                    tankState                 = GameState.GetMobileState(tank.Index);
            TurnCalculationCache           turnCalcCache             = Game.Current.Turns[GameState.Tick].CalculationCache;
            Cell                           targetCell                = turnCalcCache.CellMatrix[targetPoint];
            FiringLineMatrix               firingLinesForTanksMatrix = GameState.CalculationCache.FiringLinesForTanksMatrix;
            AttackTargetDistanceCalculator attackCalculator          = new AttackTargetDistanceCalculator(
                ElementType.TANK, firingLinesForTanksMatrix, GameState.CalculationCache, turnCalcCache);

            attackCalculator.MovementDirections = new Direction[] { finalMovementDir };
            attackCalculator.EdgeOffsets        = edgeOffsets;
            CombinedMovementAndFiringDistanceCalculation combinedDistCalc
                = attackCalculator.GetShortestAttackDistanceFromCurrentTankPosition(tank.Index,
                                                                                    targetCell);

            return(PathCalculator.GetTanksActionsOnOutgoingShortestAttackPathFromCurrentTankPosition(
                       tank.Index, combinedDistCalc, GameState.CalculationCache, keepMovingCloserOnFiringLastBullet));

            /* was:
             * DirectionalMatrix<DistanceCalculation> incomingDistanceMatrix
             *  = attackCalculator.CalculateMatrixOfShortestDistancesToTargetCell(targetCell);
             * DistanceCalculation distanceCalc = incomingDistanceMatrix[tankState];
             * return PathCalculator.GetTankActionsOnIncomingShortestPath(incomingDistanceMatrix, tankState.Dir, tankState.Pos.X, tankState.Pos.Y,
             *  targetPoint.X, targetPoint.Y, firingLinesForTanksMatrix, keepMovingCloserOnFiringLastBullet);
             */
        }
Beispiel #2
0
        public int GetAttackDistanceFromTankToTankAtPointAlongDirectionOfMovement(
            int playerIndex, int tankNumber, Point targetPoint, Direction finalMovementDir,
            EdgeOffset[] edgeOffsets)
        {
            Tank                           tank                      = Game.Current.Players[playerIndex].Tanks[tankNumber];
            MobileState                    tankState                 = GameState.GetMobileState(tank.Index);
            TurnCalculationCache           turnCalcCache             = Game.Current.Turns[GameState.Tick].CalculationCache;
            Cell                           targetCell                = turnCalcCache.CellMatrix[targetPoint];
            FiringLineMatrix               firingLinesForTanksMatrix = GameState.CalculationCache.FiringLinesForTanksMatrix;
            AttackTargetDistanceCalculator attackCalculator          = new AttackTargetDistanceCalculator(
                ElementType.TANK, firingLinesForTanksMatrix, GameState.CalculationCache, turnCalcCache);

            attackCalculator.MovementDirections = new Direction[] { finalMovementDir };
            attackCalculator.EdgeOffsets        = edgeOffsets;
            CombinedMovementAndFiringDistanceCalculation combinedDistCalc
                = attackCalculator.GetShortestAttackDistanceFromCurrentTankPosition(tank.Index,
                                                                                    targetCell);

            if (combinedDistCalc == null)
            {
                return(Constants.UNREACHABLE_DISTANCE);
            }
            else
            {
                return(combinedDistCalc.TicksTillTargetShot);
            }

            /* was:
             * DirectionalMatrix<DistanceCalculation> incomingDistanceMatrix
             *  = attackCalculator.CalculateMatrixOfShortestDistancesToTargetCell(targetCell);
             * DistanceCalculation distanceCalc = incomingDistanceMatrix[tankState];
             * return distanceCalc.Distance;
             */
        }
Beispiel #3
0
        public DirectionalMatrix <DistanceCalculation> GetCustomDistanceMatrixFromTank(
            int playerIndex, int tankNumber, int ticksWithoutFiring, Rectangle restrictedBoardArea)
        {
            Tank tank = Game.Current.Players[playerIndex].Tanks[tankNumber];
            TurnCalculationCache turnCalcCache = Game.Current.Turns[GameState.Tick].CalculationCache;

            // Don't ride over your own base!
            Base         @base   = tank.Player.Base;
            TankLocation tankLoc = turnCalcCache.TankLocationMatrix[@base.Pos];

            Rectangle[] tabooAreas = new Rectangle[] { tankLoc.TankBody };

            DistanceCalculator distanceCalculator = new DistanceCalculator();

            distanceCalculator.Walls = GameState.Walls;
            distanceCalculator.TankOuterEdgeMatrix    = GameState.CalculationCache.TankOuterEdgeMatrix;
            distanceCalculator.CellMatrix             = turnCalcCache.CellMatrix;
            distanceCalculator.TabooAreas             = tabooAreas;
            distanceCalculator.TicksWithoutFiring     = ticksWithoutFiring;
            distanceCalculator.RestrictedMovementArea = restrictedBoardArea;
            MobileState tankState = GameState.GetMobileState(tank.Index);
            DirectionalMatrix <DistanceCalculation> distanceMatrix
                = distanceCalculator.CalculateShortestDistancesFromTank(ref tankState);

            return(distanceMatrix);
        }
Beispiel #4
0
 public FiringLineMatrix(Point topLeft, int width, int height, ElementExtentType extentType,
                         TurnCalculationCache turnCalcationCache, GameStateCalculationCache gameStateCalculationCache,
                         EdgeOffsetType lastSupportedEdgeOffsetType = EdgeOffsetType.Centre)
 {
     directionalMatrixOfFiringLinesByEdgeOffset = new DirectionalMatrix <Line <FiringDistance>[]>(topLeft, width, height);
     LastSupportedEdgeOffsetType = lastSupportedEdgeOffsetType;
     ExtentType                = extentType;
     TurnCalcationCache        = turnCalcationCache;
     GameStateCalculationCache = gameStateCalculationCache;
 }
Beispiel #5
0
 /// <summary>
 /// AttackTargetDistanceCalculator constructor
 /// </summary>
 /// <param name="targetElementType">The type of element (base, tank or bullet) which is being attacked</param>
 /// <param name="firingLineMatrix">The firing line matrix must be applicable to the target element type's extent</param>
 /// <param name="gameStateCalculationCache"></param>
 /// <param name="turnCalculationCache"></param>
 public AttackTargetDistanceCalculator(
     ElementType targetElementType,
     FiringLineMatrix firingLineMatrix,
     GameStateCalculationCache gameStateCalculationCache,
     TurnCalculationCache turnCalculationCache) : this()
 {
     FiringLineMatrix          = firingLineMatrix;
     GameStateCalculationCache = gameStateCalculationCache;
     TurnCalculationCache      = turnCalculationCache;
     TargetElementType         = targetElementType;
 }
Beispiel #6
0
        public int GetAttackDistanceFromTankToHypotheticalTankAtPoint(
            int playerIndex, int tankNumber, Point targetTankPoint, EdgeOffset[] edgeOffsets)
        {
            Tank                           tank                      = Game.Current.Players[playerIndex].Tanks[tankNumber];
            MobileState                    tankState                 = GameState.GetMobileState(tank.Index);
            TurnCalculationCache           turnCalcCache             = Game.Current.Turns[GameState.Tick].CalculationCache;
            Cell                           targetCell                = turnCalcCache.CellMatrix[targetTankPoint];
            FiringLineMatrix               firingLinesForTanksMatrix = GameState.CalculationCache.FiringLinesForTanksMatrix;
            AttackTargetDistanceCalculator attackCalculator          = new AttackTargetDistanceCalculator(
                ElementType.TANK, firingLinesForTanksMatrix, GameState.CalculationCache, turnCalcCache);

            attackCalculator.MovementDirections = BoardHelper.AllRealDirections;
            attackCalculator.EdgeOffsets        = edgeOffsets;
            CombinedMovementAndFiringDistanceCalculation combinedDistCalc
                = attackCalculator.GetShortestAttackDistanceFromCurrentTankPosition(tank.Index,
                                                                                    targetCell);

            return(combinedDistCalc.TicksTillTargetShot);
        }
        public static BulletCalculation GetBulletCalculation(GameState gameState, Player you = null)
        {
            TurnCalculationCache         turnCalcCache          = Game.Current.Turns[gameState.Tick].CalculationCache;
            BulletCalculation            bulletCalc             = new BulletCalculation();
            List <BulletPathCalculation> bulletPathCalculations = new List <BulletPathCalculation>();

            for (int b = Constants.MIN_BULLET_INDEX; b <= Constants.MAX_BULLET_INDEX; b++)
            {
                MobileState bulletState = gameState.GetMobileState(b);
                if (bulletState.IsActive)
                {
                    DoBulletPathCalculations(gameState, turnCalcCache, bulletPathCalculations, b, bulletState);
                }
            }
            bulletCalc.BulletPaths = bulletPathCalculations.ToArray();

            CalculateBulletThreats(bulletCalc, gameState, you);

            return(bulletCalc);
        }
        public static Line <FiringDistance> GetFiringDistancesToPoint(Cell target, Direction directionOfMovement,
                                                                      TurnCalculationCache turnCalcCache, GameStateCalculationCache gameStateCache)
        {
            Direction outwardDirection = directionOfMovement.GetOpposite();

            Line <Point>          line       = target.LineFromCellToEdgeOfBoardByDirection[(int)outwardDirection];
            Line <FiringDistance> firingLine = new Line <FiringDistance>(line.StartOfLine, line.DirectionOfLine, line.Length);

            // The following distances are from the POINT IN FRONT OF THE TANK WHERE THE BULLET IS FIRED:
            int indexOfNextShootableWallSegment   = 0;
            int indexOfNextUnshootableWallSegment = 0;
            int firingCount = 1;

            int  prevIndexOfNextShootableWallSegment   = 0;
            int  prevIndexOfNextUnshootableWallSegment = 0;
            int  prevTicksTillTargetShot = 1;
            bool isValid = true;

            Point startingPoint = target.Position;

            /* Fake a conditional breakpoint...
             * if ((startingPoint == new Point(18, 72)) && (outwardDirection == Direction.UP))
             * {
             *  DebugHelper.LogDebugMessage("FiringDistanceCalculator", "Conditional breakpoint reached");
             * }
             */

            for (int i = 0; i < firingLine.Length; i++)
            {
                Point          tankFiringPoint = line[i];
                Point          tankCentrePoint = tankFiringPoint + Constants.TANK_OUTER_EDGE_OFFSET * outwardDirection.GetOffset();
                FiringDistance dist            = new FiringDistance
                {
                    TankFiringPoint      = tankFiringPoint,
                    StartingTankPosition = tankCentrePoint
                };
                firingLine[i] = dist;

                /* Fake a conditional breakpoint...
                 * if ((tankFiringPoint == new Point(18, 62)) && (outwardDirection == Direction.UP))
                 * {
                 *  DebugHelper.LogDebugMessage("FiringDistanceCalculator", "Conditional breakpoint reached");
                 * }
                 */

                TankLocation tankLoc;
                SegmentState segStateOnLeadingOutsideEdge = SegmentState.Clear;
                // NB: value assigned just to stop the compiler complaining

                Cell tankCentreCell = turnCalcCache.CellMatrix[tankCentrePoint];
                isValid = tankCentreCell.IsValid;
                if (isValid)
                {
                    tankLoc = turnCalcCache.TankLocationMatrix[tankCentrePoint];
                    segStateOnLeadingOutsideEdge = gameStateCache.TankOuterEdgeMatrix[tankCentrePoint][(int)directionOfMovement];
                    isValid = isValid && tankLoc.IsValid;
                }

                if (isValid)
                {
                    // This is not the closest point to the target:
                    if (segStateOnLeadingOutsideEdge == SegmentState.OutOfBounds)
                    {
                        isValid = false;
                    }
                    else
                    {
                        indexOfNextShootableWallSegment
                            = (segStateOnLeadingOutsideEdge == SegmentState.ShootableWall)
                            ? i
                            : prevIndexOfNextShootableWallSegment;
                        indexOfNextUnshootableWallSegment
                            = (segStateOnLeadingOutsideEdge == SegmentState.UnshootablePartialWall)
                            ? i
                            : prevIndexOfNextUnshootableWallSegment;

                        /* The test harness has bugs where a tank can overlap with a wall
                         * (e.g. some maps have starting positions like this).
                         * So don't consider whether there is a shootable wall at the target point.
                         * Also don't consider it anyway, because we might be shooting at
                         * a possible future point of an enemy tank, not its current point.
                         */
                        if ((segStateOnLeadingOutsideEdge == SegmentState.ShootableWall) && (i > 0))
                        {
                            firingCount++;
                        }

                        dist.IndexOfNextShootableWallSegment   = indexOfNextShootableWallSegment;
                        dist.IndexOfNextUnshootableWallSegment = indexOfNextUnshootableWallSegment;

                        // Save these variables for the next tick, as they are about to be overwritten:
                        prevIndexOfNextShootableWallSegment   = indexOfNextShootableWallSegment;
                        prevIndexOfNextUnshootableWallSegment = indexOfNextUnshootableWallSegment;

                        // Calculate the number of ticks required to shoot the target:
                        int  indexOfTankFiringPoint        = i;
                        bool canTankMoveStill              = indexOfTankFiringPoint > indexOfNextUnshootableWallSegment;
                        int  ticksTillTargetShot           = 0;
                        int  ticksTillLastShotFired        = 0;
                        FiringActionSet[] firingActionSets = new FiringActionSet[firingCount];
                        dist.FiringActionsSets = firingActionSets;
                        int  firingActionSetIndex = 0;
                        bool isFirstShot          = true;
                        while (true)
                        {
                            bool isFinalShot = indexOfNextShootableWallSegment == 0;

                            // Calculate the number of ticks to destroy the target:
                            int distanceToNewShootableWall = indexOfTankFiringPoint - indexOfNextShootableWallSegment + 1;
                            int ticksToShootNextWall       = 1 + (distanceToNewShootableWall >> 1);
                            // Ticks = 1 tick to fire + Ceiling((distanceToNextShootableWallSegment - 1)/2.0)
                            //       = 1 + floor( distanceToNextShootableWallSegment / 2.0 )
                            //       = 1 + distanceToNextShootableWallSegment / 2
                            //       = 1 + (distanceToNextShootableWallSegment >> 1)
                            ticksTillTargetShot    += ticksToShootNextWall;
                            ticksTillLastShotFired += (isFinalShot ? 1 : ticksToShootNextWall);

                            if (isFirstShot)
                            {
                                dist.DoesFiringLineStartWithLongDistanceShot = (i > 1) && (distanceToNewShootableWall > 2);
                                // Because a wall two away (or less) can be reached through move, shoot, move (shortest path steps)
                                // in the same time as shoot, move, move.
                                isFirstShot = false;
                            }

                            int newIndexOfTankFiringPoint = indexOfTankFiringPoint;
                            if (canTankMoveStill)
                            {
                                newIndexOfTankFiringPoint = indexOfTankFiringPoint - ticksToShootNextWall + 1;
                                // stationary for 1 tick (to fire), then moving closer on remaining ticks
                                if (newIndexOfTankFiringPoint < indexOfNextUnshootableWallSegment)
                                {
                                    // The tank can't get through an unshootable wall segment:
                                    newIndexOfTankFiringPoint = indexOfNextUnshootableWallSegment;
                                    canTankMoveStill          = false;
                                }
                            }
                            FiringActionSet firingActionSet = new FiringActionSet(
                                (byte)indexOfTankFiringPoint,
                                (byte)ticksToShootNextWall,
                                numberOfMovesMade: (byte)(indexOfTankFiringPoint - newIndexOfTankFiringPoint),
                                canMoveOnceBeforeFiring: canTankMoveStill && (distanceToNewShootableWall % 2 == 0),
                                isFinalShot: (indexOfNextShootableWallSegment == 0)
                                );
                            firingActionSets[firingActionSetIndex] = firingActionSet;
                            firingActionSetIndex++;

                            if (indexOfNextShootableWallSegment == 0)
                            {
                                break;
                            }
                            indexOfTankFiringPoint          = newIndexOfTankFiringPoint;
                            indexOfNextShootableWallSegment = firingLine[indexOfNextShootableWallSegment - 1].IndexOfNextShootableWallSegment;
                        }
                        dist.TicksTillTargetShot    = ticksTillTargetShot;
                        dist.TicksTillLastShotFired = ticksTillLastShotFired;
                        dist.EndingTankPosition     = line[indexOfTankFiringPoint] + Constants.TANK_OUTER_EDGE_OFFSET * outwardDirection.GetOffset();

                        /* If the next closer space is 1 tick closer,
                         * then the tank could just move there along a normal non-firing line path:
                         * [The exception is if it is blocked by an unshootable partial wall]
                         */
                        if ((ticksTillTargetShot == prevTicksTillTargetShot + 1) &&
                            ((i > prevIndexOfNextUnshootableWallSegment)))
                        {
                            dist.CanMoveOrFire = true;
                        }
                        Debug.Assert(ticksTillTargetShot >= prevTicksTillTargetShot);
                        prevTicksTillTargetShot = ticksTillTargetShot;
                    }
                }

                if (isValid)
                {
                    dist.IsValid = true;
                }
                else
                {
                    dist.TicksTillTargetShot = Constants.UNREACHABLE_DISTANCE;;
                }
            }

            return(firingLine);
        }
        private static void DoBulletPathCalculations(GameState gameState,
                                                     TurnCalculationCache turnCalcCache, List <BulletPathCalculation> bulletPathCalculations,
                                                     int bulletIndex, MobileState bulletState)
        {
            BulletPathCalculation bulletPathCalc = new BulletPathCalculation
            {
                Bullet      = Game.Current.Elements[bulletIndex] as Bullet,
                BulletState = bulletState
            };

            bulletPathCalculations.Add(bulletPathCalc);

            BulletPathPoint[] bulletPathPoints = new BulletPathPoint[500];
            int bulletPathPointCount           = 0;

            Cell         cell = turnCalcCache.CellMatrix[bulletState.Pos];
            Line <Point> pointsInBulletPath = cell.LineFromCellToEdgeOfBoardByDirection[(int)bulletState.Dir];
            int          tickOffset         = 0;

            for (int i = 1; i < pointsInBulletPath.Length; i++)
            {
                Point bulletPoint     = pointsInBulletPath[i];
                Cell  bulletPointCell = turnCalcCache.CellMatrix[bulletPoint];
                if (!bulletPointCell.IsValid)
                {
                    break;
                }

                if (gameState.Walls[bulletPoint])
                {
                    bulletPathCalc.TicksTillBulletDestroyed = tickOffset;
                    break;
                }
                TankLocation tankLoc    = turnCalcCache.TankLocationMatrix[bulletPoint];
                Rectangle    dangerArea = tankLoc.TankBody;

                BulletPathPoint bulletPathPoint = new BulletPathPoint
                {
                    BulletPoint   = pointsInBulletPath[i],
                    DangerArea    = dangerArea,
                    Tick          = gameState.Tick + tickOffset,
                    TicksToEscape = tickOffset,
                    MovementPhase = (i - 1) % 2
                };
                bulletPathPoints[bulletPathPointCount] = bulletPathPoint;
                bulletPathPointCount++;
                for (int p = 0; p < Constants.PLAYERS_PER_GAME; p++)
                {
                    Base @base = Game.Current.Players[p].Base;
                    if (bulletPoint == @base.Pos)
                    {
                        bulletPathCalc.BaseThreatened           = @base;
                        bulletPathCalc.TicksTillBulletDestroyed = tickOffset;
                    }
                }
                if (i % 2 == 0)
                {
                    tickOffset++;
                }
            }

            Array.Resize(ref bulletPathPoints, bulletPathPointCount);
            bulletPathCalc.BulletPathPoints = bulletPathPoints;
        }
        private static void CalculateBulletThreats(BulletCalculation bulletCalc, GameState gameState, Player you)
        {
            TurnCalculationCache turnCalcCache = Game.Current.Turns[gameState.Tick].CalculationCache;

            Tank[] tanks;
            if (you != null)
            {
                tanks = you.Tanks;
            }
            else
            {
                tanks = Game.Current.Tanks;
            }
            foreach (Tank tank in tanks)
            {
                MobileState tankState = gameState.GetMobileState(tank.Index);
                foreach (BulletPathCalculation bulletPathCalc in bulletCalc.BulletPaths)
                {
                    List <BulletThreat> bulletThreats = new List <BulletThreat>();
                    foreach (BulletPathPoint bulletPathPoint in bulletPathCalc.BulletPathPoints)
                    {
                        if (bulletPathPoint.DangerArea.ContainsPoint(tankState.Pos))
                        {
                            BulletThreat bulletThreat = new BulletThreat(bulletPathCalc)
                            {
                                FiringTank     = bulletPathCalc.Bullet.Tank,
                                TankThreatened = tank
                            };
                            bulletThreats.Add(bulletThreat);

                            // Take the bullet on:
                            FiringLineMatrix firingLineMatrix = gameState.CalculationCache.FiringLinesForPointsMatrix;
                            AttackTargetDistanceCalculator attackCalculator = new AttackTargetDistanceCalculator(
                                ElementType.BULLET, firingLineMatrix, gameState.CalculationCache, turnCalcCache);
                            attackCalculator.MovementDirections = new Direction[]
                            {
                                bulletPathCalc.BulletState.Dir.GetOpposite()
                            };
                            Cell bulletCell = turnCalcCache.CellMatrix[bulletPathCalc.BulletState.Pos];
                            DirectionalMatrix <DistanceCalculation> distanceCalcs
                                = attackCalculator.CalculateMatrixOfShortestDistancesToTargetCell(bulletCell);
                            DistanceCalculation distanceCalc = distanceCalcs[tankState];
                            if (distanceCalc.Distance <= bulletPathPoint.TicksToEscape)
                            {
                                Node[] nodes = PathCalculator.GetIncomingNodesOnShortestPath(
                                    distanceCalcs, tankState.Dir, tankState.Pos.X, tankState.Pos.Y,
                                    bulletPathCalc.BulletState.Pos.X, bulletPathCalc.BulletState.Pos.Y,
                                    firingLineMatrix, keepMovingCloserOnFiringLastBullet: true);
                                bulletThreat.NodePathToTakeOnBullet = nodes;
                                TankAction[] tankActions
                                    = PathCalculator.GetTankActionsOnIncomingShortestPath(distanceCalcs,
                                                                                          tankState.Dir, tankState.Pos.X, tankState.Pos.Y,
                                                                                          bulletPathCalc.BulletState.Pos.X, bulletPathCalc.BulletState.Pos.Y,
                                                                                          firingLineMatrix, keepMovingCloserOnFiringLastBullet: true);
                                bulletThreat.TankActionsToTakeOnBullet = tankActions;
                            }

                            // Move off his path in one direction:
                            Point tankOffset           = tankState.Pos - bulletPathCalc.BulletState.Pos;
                            Point bulletMovementOffset = bulletPathCalc.BulletState.Dir.GetOffset();

                            Point     newTankPoint1;
                            Point     newTankPoint2;
                            Direction newTankDir1;
                            Direction newTankDir2;

                            if (bulletPathCalc.BulletState.Dir.ToAxis() == Axis.Horizontal)
                            {
                                newTankPoint1 = new Point(
                                    (short)tankState.Pos.X,
                                    (short)(bulletPathCalc.BulletState.Pos.Y - Constants.TANK_OUTER_EDGE_OFFSET));
                                newTankDir1   = Direction.UP;
                                newTankPoint2 = new Point(
                                    (short)tankState.Pos.X,
                                    (short)(bulletPathCalc.BulletState.Pos.Y + Constants.TANK_OUTER_EDGE_OFFSET));
                                newTankDir2 = Direction.DOWN;
                            }
                            else
                            {
                                newTankPoint1 = new Point(
                                    (short)(bulletPathCalc.BulletState.Pos.X - Constants.TANK_OUTER_EDGE_OFFSET),
                                    (short)tankState.Pos.Y);
                                newTankDir1   = Direction.LEFT;
                                newTankPoint2 = new Point(
                                    (short)(bulletPathCalc.BulletState.Pos.X + Constants.TANK_OUTER_EDGE_OFFSET),
                                    (short)tankState.Pos.Y);
                                newTankDir2 = Direction.RIGHT;
                            }

                            DirectionalMatrix <DistanceCalculation> tankDistanceCalcs
                                = gameState.CalculationCache.GetDistanceMatrixFromTankByTankIndex(tank.Index);

                            distanceCalc = tankDistanceCalcs[newTankDir1, newTankPoint1.X, newTankPoint1.Y];
                            if (distanceCalc.Distance <= bulletPathPoint.TicksToEscape)
                            {
                                Node[] shortestPathsIn1Direction
                                    = PathCalculator.GetOutgoingNodesOnShortestPath(tankDistanceCalcs,
                                                                                    newTankDir1, newTankPoint1.X, newTankPoint1.Y);
                                bulletThreat.LateralMoveInOneDirection = shortestPathsIn1Direction;
                                TankAction[] tankActionsIn1Direction
                                    = PathCalculator.GetTankActionsOnOutgoingShortestPath(tankDistanceCalcs,
                                                                                          newTankDir1, newTankPoint1.X, newTankPoint1.Y);
                                bulletThreat.TankActionsForLateralMoveInOneDirection = tankActionsIn1Direction;
                            }

                            distanceCalc = tankDistanceCalcs[newTankDir2, newTankPoint2.X, newTankPoint2.Y];
                            if (distanceCalc.Distance <= bulletPathPoint.TicksToEscape)
                            {
                                Node[] shortestPathsInOtherDirection = PathCalculator.GetOutgoingNodesOnShortestPath(tankDistanceCalcs,
                                                                                                                     newTankDir2, newTankPoint2.X, newTankPoint2.Y);
                                bulletThreat.LateralMoveInOtherDirection = shortestPathsInOtherDirection;
                                TankAction[] tankActionsInOtherDirection
                                    = PathCalculator.GetTankActionsOnOutgoingShortestPath(tankDistanceCalcs,
                                                                                          newTankDir2, newTankPoint2.X, newTankPoint2.Y);
                                bulletThreat.TankActionsForLateralMoveInOtherDirection = tankActionsInOtherDirection;
                            }
                        }
                    }
                    bulletPathCalc.BulletThreats = bulletThreats.ToArray();
                }
            }
        }
        public MoveResult EvaluateLeafNodeMoveWhenTanksAreNotCloseToLockDown(Move move,
                                                                             MobileState tankState_i, MobileState tankState_j, Direction horizDir, Direction vertDir)
        {
            MoveResult moveResult = new MoveResult(move);

            // Find the Voronoi point on the straight line between the two tanks as an estimate of where the lock-down could take place:
            Point[] zigZagPoints       = tankState_i.Pos.GetPointsOnZigZagLineToTargetPoint(tankState_j.Pos);
            int     minDiff            = Constants.UNREACHABLE_DISTANCE;
            int     distToMinDiffPoint = Constants.UNREACHABLE_DISTANCE;
            Point   minDiffPoint       = new Point();

            foreach (Point pointOnLine in zigZagPoints)
            {
                int dist1    = GetDistanceFromTankToPoint(move.p, move.i, move.dir1, pointOnLine);
                int dist2    = GetDistanceFromTankToPoint(move.pBar, move.j, move.dir1.GetOpposite(), pointOnLine);
                int distDiff = Math.Abs(dist1 - dist2);
                if (distDiff < minDiff)
                {
                    minDiff            = distDiff;
                    minDiffPoint       = pointOnLine;
                    distToMinDiffPoint = dist1;
                }
            }

            if (minDiff == Constants.UNREACHABLE_DISTANCE)
            {
                moveResult.EvaluationOutcome = ScenarioEvaluationOutcome.Invalid;
                return(moveResult);
            }

            Rectangle boardBoundary = new Rectangle(0, 0, (short)(GameState.Walls.Width - 1), (short)(GameState.Walls.Height - 1));

            Point estimated_pos_pBar_j = minDiffPoint + move.dir1.GetOffset(4);  // 4 points away on one side

            estimated_pos_pBar_j = estimated_pos_pBar_j.BringIntoBounds(boardBoundary);

            Point estimated_pos_p_i = minDiffPoint + move.dir1.GetOpposite().GetOffset(4);  // 4 points away on the other side

            estimated_pos_p_i = estimated_pos_p_i.BringIntoBounds(boardBoundary);

            int slack_lock_down = distToMinDiffPoint;

            int A_p_iBar
                = GetAttackDistanceFromTankToTankAtPointAlongDirectionOfMovement(
                      move.p, move.iBar, estimated_pos_pBar_j, move.dir2, TankHelper.EdgeOffsets);
            int A_pBar_jBar
                = GetAttackDistanceFromTankToTankAtPointAlongDirectionOfMovement(
                      move.pBar, move.jBar, estimated_pos_p_i, move.dir3, TankHelper.EdgeOffsets);
            int slack_Bar = A_p_iBar - A_pBar_jBar;
            int slack     = Math.Max(slack_Bar, slack_lock_down);

            // slack_Bar is symmetrical. If it's above zero, then we are in trouble.
            // If below, then he's in trouble. So choose actions based on this slack:
            if (slack_Bar < 0)
            {
                // Move tank p_i closer to the estimated interception point with pBar_j:
                TankAction[] tankActions_move_p_i = GetTankActionsToMoveToPoint(move.p, move.i, move.dir1, estimated_pos_pBar_j);
                if (tankActions_move_p_i.Length > 0)
                {
                    moveResult.SetTankActionRecommendation(move.p, move.i,
                                                           new TankActionRecommendation
                    {
                        IsAMoveRecommended    = true,
                        RecommendedTankAction = tankActions_move_p_i[0]
                    });
                }

                // Move tank pBar_j further away from attackers p_i and p_iBar:
                // (Also ideally towards the edge of the board, if that's not where the attack is coming from, to act as a decoy)
                int        maxDistanceFromAttackers = int.MinValue;
                TankAction escapeAction             = TankAction.NONE;

                foreach (Direction escapeDir in BoardHelper.AllRealDirections)
                {
                    Point adjacentPoint = tankState_j.Pos + escapeDir.GetOffset();
                    TurnCalculationCache turnCalcCache = Game.Current.Turns[GameState.Tick].CalculationCache;
                    TankLocation         tankLoc       = turnCalcCache.TankLocationMatrix[adjacentPoint];
                    if (tankLoc.IsValid)
                    {
                        int adj_moveDistance_pBar_j = GetDistanceFromTankToPoint(move.pBar, move.j, escapeDir, adjacentPoint);
                        if (adj_moveDistance_pBar_j < Constants.UNREACHABLE_DISTANCE)
                        {
                            int adj_attackDistance_p_i = GetAttackDistanceFromTankToTankAtPointAlongDirectionOfMovement(
                                move.p, move.i, adjacentPoint, move.dir1, TankHelper.EdgeOffsets);
                            int adj_attackDistance_p_iBar = GetAttackDistanceFromTankToTankAtPointAlongDirectionOfMovement(
                                move.p, move.iBar, adjacentPoint, move.dir2, TankHelper.EdgeOffsets);
                            int adj_attackDistance_SUM = adj_attackDistance_p_i + adj_attackDistance_p_iBar - adj_moveDistance_pBar_j;

                            if (adj_attackDistance_SUM < maxDistanceFromAttackers)
                            {
                                TankAction[] escapeActions = GetTankActionsToMoveToPoint(move.pBar, move.j, escapeDir, adjacentPoint);
                                if (escapeActions.Length > 0)
                                {
                                    maxDistanceFromAttackers = adj_attackDistance_SUM;
                                    escapeAction             = escapeActions[0];
                                }
                            }
                        }
                    }
                }

                if (escapeAction != TankAction.NONE)
                {
                    TankActionRecommendation tankActRec = new TankActionRecommendation
                    {
                        IsAMoveRecommended    = true,
                        RecommendedTankAction = escapeAction
                    };
                    moveResult.SetTankActionRecommendation(move.pBar, move.j, tankActRec);
                }
            }

            // Move tank p_iBar closer to lock down target pBar_j (don't attack yet, because lock-down hasn't occurred):
            TankAction[] tankActions_p_iBar = GetTankActionsToMoveToPoint(move.p, move.iBar, move.dir2, estimated_pos_pBar_j);
            if (tankActions_p_iBar.Length > 0)
            {
                TankActionRecommendation tankActRec = new TankActionRecommendation
                {
                    IsAMoveRecommended    = true,
                    RecommendedTankAction = tankActions_p_iBar[0],
                };
                moveResult.SetTankActionRecommendation(move.p, move.iBar, tankActRec);
            }

            // Move tank pBar_j closer to lock down target p_i:
            TankAction[] tankActions_pBar_j = GetTankActionsToMoveToPoint(move.pBar, move.j, move.dir3, estimated_pos_p_i);
            if (tankActions_pBar_j.Length > 0)
            {
                TankActionRecommendation tankActRec = new TankActionRecommendation
                {
                    IsAMoveRecommended    = true,
                    RecommendedTankAction = tankActions_pBar_j[0],
                };
                moveResult.SetTankActionRecommendation(move.pBar, move.j, tankActRec);
            }

            if (slack < 0)
            {
                moveResult.EvaluationOutcome = ScenarioEvaluationOutcome.Close;
            }
            else
            {
                moveResult.EvaluationOutcome = ScenarioEvaluationOutcome.Possible;
            }
            return(moveResult);
        }
        private static void GenerateBulletTimeline(GameState gameState, BulletSituation bulletSituation)
        {
            MobileState          bulletState       = bulletSituation.BulletStateAtTimeOfFiring;
            TurnCalculationCache turnCalcCache     = Game.Current.Turns[bulletSituation.TickFired].CalculationCache;
            Direction            bulletMovementDir = bulletSituation.BulletMovementDir;
            Direction            oppositeDir       = bulletMovementDir.GetOpposite();
            Cell         cell = turnCalcCache.CellMatrix[bulletState.Pos];
            Line <Point> pointsInBulletPath = cell.LineFromCellToEdgeOfBoardByDirection[(int)bulletState.Dir];

            int maxTickCount = (pointsInBulletPath.Length + 2) / 2;

            BulletCalculationByTick[] bulletCalcsByTick = new BulletCalculationByTick[maxTickCount];
            int tickOffset;

            for (tickOffset = 0; tickOffset < maxTickCount; tickOffset++)
            {
                BulletCalculationByTick bulletCalc = new BulletCalculationByTick
                {
                    Tick       = bulletSituation.TickFired + tickOffset,
                    TickOffset = tickOffset
                };
                bulletCalcsByTick[tickOffset] = bulletCalc;
                bool      areTanksAtRisk = false;
                Rectangle dangerRect     = new Rectangle();
                Point[]   bulletPoints   = new Point[2];
                Point[,] adjacentTankPointsByRotationTypeAndPhase = new Point[Constants.ROTATION_TYPE_COUNT, 2];
                int bulletPhase;
                int startPhase = (tickOffset == 0) ? 1 : 0;
                for (bulletPhase = startPhase; bulletPhase < 2; bulletPhase++)
                {
                    int pointIndex = 2 * tickOffset - 1 + bulletPhase;

                    if (pointIndex >= pointsInBulletPath.Length)
                    {
                        // Bullet moved off the edge of the board:
                        bulletCalc.IsDestroyed = true;
                        bulletSituation.TickOffsetWhenTankCanFireAgain = tickOffset;
                        break;
                    }

                    Point bulletPoint = pointsInBulletPath[pointIndex];
                    bulletPoints[bulletPhase] = bulletPoint;

                    Cell bulletCell = turnCalcCache.CellMatrix[bulletPoint];
                    if ((!bulletCell.IsValid) || gameState.Walls[bulletPoint])
                    {
                        bulletCalc.IsDestroyed = true;
                        bulletSituation.TickOffsetWhenTankCanFireAgain = tickOffset;
                        break;
                    }

                    areTanksAtRisk = true;
                    TankLocation tankLoc = turnCalcCache.TankLocationMatrix[bulletPoint];

                    if (bulletPhase == 0 || tickOffset == 0)
                    {
                        dangerRect = tankLoc.TankBody;
                    }
                    else
                    {
                        dangerRect = dangerRect.Merge(tankLoc.TankBody);
                    }

                    foreach (RotationType rotationType in BoardHelper.AllRotationTypes)
                    {
                        Direction rotatedDir = bulletMovementDir.GetRotatedDirection(rotationType);
                        switch (rotationType)
                        {
                        case RotationType.Clockwise:
                        case RotationType.AntiClockwise:
                            // A bit more complex than expected... must move the escape point forwards so that ticks to escape is correct:
                            adjacentTankPointsByRotationTypeAndPhase[(int)rotationType, bulletPhase]
                                = bulletPoint
                                  + rotatedDir.GetOffset(Constants.TANK_OUTER_EDGE_OFFSET)
                                  + bulletMovementDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK);
                            break;

                        case RotationType.None:
                            adjacentTankPointsByRotationTypeAndPhase[(int)rotationType, bulletPhase]
                                = bulletPoint
                                  + rotatedDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK);
                            // Add the extra 2 spaces, so that the tank still has a turn in which to fire, since bullets move first
                            break;

                        case RotationType.OneEightyDegrees:
                            adjacentTankPointsByRotationTypeAndPhase[(int)rotationType, bulletPhase]
                                = bulletPoint
                                  + rotatedDir.GetOffset(Constants.TANK_OUTER_EDGE_OFFSET + 1);
                            // Add the extra +1 offset, so the tank can also turn to face the firing tank
                            break;
                        }
                    }
                    // The safe position must be
                }
                bulletCalc.AreTanksAtRisk = areTanksAtRisk;
                if (areTanksAtRisk)
                {
                    bulletCalc.TankCentrePointsThatDie = dangerRect;


                    if (tickOffset != 0)
                    {
                        Point pointMovingInBehindBulletFacingFiringTank
                            = adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.OneEightyDegrees, 0];
                        bulletCalc.ClosestTankStateMovingInBehindBulletFacingFiringTank
                            = new MobileState(pointMovingInBehindBulletFacingFiringTank, oppositeDir, isActive: true);
                    }

                    if (bulletPhase == 2)
                    {
                        if (tickOffset == 0)
                        {
                            // Only include calculations relative to the second bullet point:
                            Point pointMovingInBehindBulletFacingFiringTank
                                = adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.OneEightyDegrees, 1];

                            bulletCalc.ClosestTankStateMovingInBehindBulletFacingFiringTank
                                = new MobileState(pointMovingInBehindBulletFacingFiringTank, oppositeDir, isActive: true);

                            bulletCalc.BulletPoints = new Point[]
                            {
                                bulletPoints[1]
                            };
                            bulletCalc.ClosestTankCentrePointsThatSurviveAntiClockwise = new Point[]
                            {
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 1]
                            };
                            bulletCalc.ClosestTankCentrePointsThatSurviveClockwise = new Point[]
                            {
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 1]
                            };
                            bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn = new MobileState[]
                            {
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.None, 1],
                                    oppositeDir,
                                    isActive: true)
                            };
                            bulletCalc.ClosestTankStatesThatCanShootBullet = new MobileState[]
                            {
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 1]
                                    + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                    bulletMovementDir.GetRotatedDirection(RotationType.Clockwise),
                                    isActive: true),
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 1]
                                    + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                    bulletMovementDir.GetRotatedDirection(RotationType.AntiClockwise),
                                    isActive: true),
                                bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn[0]
                            };
                        }
                        else
                        {
                            // Include calculations relative to the first and second bullet point:
                            bulletCalc.BulletPoints = new Point[]
                            {
                                bulletPoints[0],
                                bulletPoints[1]
                            };
                            bulletCalc.ClosestTankCentrePointsThatSurviveAntiClockwise = new Point[]
                            {
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 0],
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 1]
                            };
                            bulletCalc.ClosestTankCentrePointsThatSurviveClockwise = new Point[]
                            {
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 0],
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 1]
                            };
                            bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn = new MobileState[]
                            {
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.None, 0],
                                    oppositeDir,
                                    isActive: true),
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.None, 1],
                                    oppositeDir,
                                    isActive: true)
                            };
                            bulletCalc.ClosestTankStatesThatCanShootBullet = new MobileState[]
                            {
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 1]
                                    + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                    bulletMovementDir.GetRotatedDirection(RotationType.Clockwise),
                                    isActive: true),
                                new MobileState(
                                    adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 1]
                                    + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                    bulletMovementDir.GetRotatedDirection(RotationType.AntiClockwise),
                                    isActive: true),
                                bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn[0],
                                bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn[1]
                            };
                        }
                    }
                    else
                    {
                        // The second bullet point never occurred as the bullet was destroyed on the first one.
                        // So only include points and tank states relative to the first bullet point:
                        bulletCalc.BulletPoints = new Point[]
                        {
                            bulletPoints[0]
                        };
                        bulletCalc.ClosestTankCentrePointsThatSurviveAntiClockwise = new Point[]
                        {
                            adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 0]
                        };
                        bulletCalc.ClosestTankCentrePointsThatSurviveClockwise = new Point[]
                        {
                            adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 0]
                        };
                        MobileState headOnConfrontationState = new MobileState(
                            adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.None, 0],
                            oppositeDir,
                            isActive: true);
                        bulletCalc.ClosestTankStatesThatCanShootBulletHeadOn = new MobileState[]
                        {
                            headOnConfrontationState
                        };
                        bulletCalc.ClosestTankStatesThatCanShootBullet = new MobileState[]
                        {
                            new MobileState(
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.AntiClockwise, 0]
                                + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                bulletMovementDir.GetRotatedDirection(RotationType.Clockwise),
                                isActive: true),
                            new MobileState(
                                adjacentTankPointsByRotationTypeAndPhase[(int)RotationType.Clockwise, 0]
                                + oppositeDir.GetOffset(Constants.TANK_EXTENT_OFFSET + Constants.BULLET_MOVEMENTS_PER_TICK),
                                bulletMovementDir.GetRotatedDirection(RotationType.Clockwise),
                                isActive: true),
                            headOnConfrontationState
                        };
                    }
                }
                if (bulletCalc.IsDestroyed)
                {
                    break;
                }
            }

            if (tickOffset + 1 < maxTickCount)
            {
                Array.Resize(ref bulletCalcsByTick, tickOffset + 1);
            }
            bulletSituation.BulletCalculationsByTick = bulletCalcsByTick;
        }