コード例 #1
0
ファイル: FleetDetail.cs プロジェクト: ekolis/stars-nova
        /// <Summary>
        /// Provides a list of objects within a certain distance from a position,
        /// ordered by distance.
        ///
        /// Copied from StarMap.cs (should probably make this a utility)
        /// </Summary>
        /// <param name="position">Starting Point for the search.</param>
        /// <returns>A list of Fleet and Star objects.</returns>
        private List <Mappable> FindNearObjects(NovaPoint position)
        {
            List <Mappable> nearObjects = new List <Mappable>();

            foreach (FleetIntel report in clientData.EmpireState.FleetReports.Values)
            {
                if (!report.IsStarbase)
                {
                    if (PointUtilities.IsNear(report.Position, position))
                    {
                        nearObjects.Add(report);
                    }
                }
            }

            foreach (StarIntel report in clientData.EmpireState.StarReports.Values)
            {
                if (PointUtilities.IsNear(report.Position, position))
                {
                    nearObjects.Add(report);
                }
            }

            // nearObjects.Sort(ItemSorter);
            return(nearObjects);
        }
コード例 #2
0
        /// <summary>
        /// Attempt an attack.
        /// </summary>
        /// <param name="allAttacks">A list of WeaponDetails representing a round of attacks.</param>
        private bool ProcessAttack(WeaponDetails attack)
        {
            // First, check that the target stack we originally identified has not
            // been destroyed (actually, the stack still exists at this point but
            // it may have no ship tokens left). In which case, don't bother trying to
            // fire this weapon system (we'll wait until the next battle clock
            // "tick" and re-target then).
            if (attack.TargetStack == null || attack.TargetStack.IsDestroyed)
            {
                return(false);
            }

            if (attack.SourceStack == null || attack.SourceStack.IsDestroyed)
            {
                // Report.Error("attacking stack no longer exists");
                return(false);
            }

            // If the target stack is not within the range of this weapon system
            // then there is no point in trying to fire it.
            if (PointUtilities.Distance(attack.SourceStack.Position, attack.TargetStack.Position) > attack.Weapon.Range)
            {
                return(false);
            }

            // Target is valid; execute attack.
            ExecuteAttack(attack);

            return(true);
        }
コード例 #3
0
        /// <summary>
        /// Check if the fleet hits the minefiled.
        /// </summary>
        /// <remarks>
        /// The probability of hitting a mine is 0.3% per light year traveled for each
        /// warp over the safe speed.
        /// TODO (priority 3) - reference required.
        ///
        /// Example: A fleet traveling at Warp 9 has a 1.5% chance per light year
        /// traveled in a turn.  Traveling 10 light years through the Minefield that
        /// turn, the fleet has a 10.5% chance of triggering a mine.
        /// </remarks>
        /// <param name="fleet">The moving fleet.</param>
        /// <param name="minefield">The minefield being traversed.</param>
        /// <returns>true if the minefield is hit.</returns>
        private bool CheckForHit(Fleet fleet, Minefield minefield)
        {
            // Calculate how long we are going to be in the Minefield. This is the
            // lesser of the distance to the next waypoint and the radius of the
            // field.

            NovaPoint currentPosition = fleet.Position;
            Waypoint  targetWaypoint  = fleet.Waypoints[0];
            NovaPoint targetPosition  = targetWaypoint.Position;

            double travelDistance = PointUtilities.Distance(currentPosition, targetPosition);

            if (minefield.Radius > (int)travelDistance)
            {
                travelDistance = minefield.Radius;
            }

            double speeding    = fleet.Speed - minefield.SafeSpeed;
            double probability = (0.03 * travelDistance * speeding) * 100;
            double dice        = random.Next(0, 100);

            if (dice < probability)
            {
                return(true);
            }

            return(false);
        }
コード例 #4
0
 private Point GetLookAheadLocation(Point currentLocation, Direction direction)
 {
     return(direction.Id switch
     {
         'U' => PointUtilities.MoveUp(currentLocation),
         'R' => PointUtilities.MoveRight(currentLocation),
         'D' => PointUtilities.MoveDown(currentLocation),
         'L' => PointUtilities.MoveLeft(currentLocation),
         _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, "Unexpected direction")
     });
コード例 #5
0
        public StarIntel Scout(List <StarIntel> excludedStars)
        {
            bool missionAccepted = false;

            // Find a star to scout
            StarIntel starToScout = CloesestStar(this.fleet, excludedStars);

            if (starToScout != null)
            {
                // Do we need fuel first?
                double fuelRequired = 0.0;
                Fleet  nearestFuel  = ClosestFuel(fleet);
                if (!fleet.CanRefuel)
                {
                    // Can not make fuel, so how much fuel is required to scout and then refuel?
                    if (nearestFuel != null)
                    {
                        int    bestWarp        = 6; // FIXME (priority 4) - what speed to scout at?
                        double bestSpeed       = bestWarp * bestWarp;
                        double speedSquared    = bestSpeed * bestSpeed;
                        double fuelConsumption = fleet.FuelConsumption(bestWarp, clientState.EmpireState.Race);
                        double distanceSquared = PointUtilities.DistanceSquare(fleet.Position, starToScout.Position); // to the stars
                        distanceSquared += PointUtilities.DistanceSquare(starToScout.Position, nearestFuel.Position); // and back to fuel (minimum)
                        double time = Math.Sqrt(distanceSquared / speedSquared);
                        fuelRequired = time * fuelConsumption;
                    }
                    else
                    {
                        // OMG there is no fuel! - just keep scouting then?
                    }
                }


                if (fleet.FuelAvailable > fuelRequired)
                {
                    // Fuel is no problem
                    SendFleet(starToScout, fleet, new NoTask());
                    missionAccepted = true;
                }
                else
                {
                    // Refuel before scouting further
                    SendFleet(nearestFuel, fleet, new NoTask());
                }
            }

            if (missionAccepted)
            {
                return(starToScout);
            }
            else
            {
                return(null);
            }
        }
コード例 #6
0
        public void TestIsNearDoesNotChangeParams()
        {
            NovaPoint a = new NovaPoint(1, 2);
            NovaPoint b = new NovaPoint(3, 4);

            bool testNear = PointUtilities.IsNear(a, b);

            Assert.IsTrue(a.X == 1);
            Assert.IsTrue(a.Y == 2);
            Assert.IsTrue(b.X == 3);
            Assert.IsTrue(b.Y == 4);
        }
コード例 #7
0
ファイル: BattleEngineTest.cs プロジェクト: ekolis/stars-nova
        public void Test5PositionStacks()
        {
            battleEngine.PositionStacks(zoneStacks);

            double distance = 0;

            Fleet stackA = zoneStacks[0] as Fleet;
            Fleet stackB = zoneStacks[1] as Fleet;

            distance = PointUtilities.Distance(stackA.Position, stackB.Position);
            Assert.Greater((int)distance, 3); // changed from Global.MaxRange -1 to 3 due to reduction in battle board size from Ken's 20x20 to Stars! 10x10 - Dan 07 Jul 11
        }
コード例 #8
0
        /// <summary>
        /// Determine if a fleet is within a Minefield. The fleet is inside the
        /// circle if the distance between the field and the center of the field is
        /// less than the radius of the field.
        /// </summary>
        private bool IsInField(Fleet fleet, Minefield minefield)
        {
            // If we are travelling at a "safe" speed we can just pretend we are
            // not in a Minefield.

            if (fleet.Speed <= minefield.SafeSpeed)
            {
                return(false);
            }

            double distance = PointUtilities.Distance(fleet.Position, minefield.Position);

            if (distance < minefield.Radius)
            {
                return(true);
            }

            return(false);
        }
コード例 #9
0
        /// <summary>
        /// Return the closest refuelling point.
        /// </summary>
        /// <param name="fleet">The fleet looking for fuel.</param>
        /// <returns>The closest fleet that can refuel (normally a star base).</returns>
        private Fleet ClosestFuel(Fleet customer)
        {
            if (customer == null)
            {
                return(null);
            }

            // initialise the list of fuel stations, if null.
            if (FuelStations == null)
            {
                FuelStations = new FleetList();
                foreach (Fleet pump in clientState.EmpireState.OwnedFleets.Values)
                {
                    if (pump.CanRefuel)
                    {
                        FuelStations.Add(pump);
                    }
                }
            }

            // if there are still no fuel stations, bug out
            if (FuelStations.Count == 0)
            {
                return(null);
            }

            Fleet  closestFuelSoFar   = null;
            double minRefulerDistance = double.MaxValue;

            foreach (Fleet pump in FuelStations.Values)
            {
                double distSquare = PointUtilities.DistanceSquare(pump.Position, customer.Position);
                if (distSquare < minRefulerDistance)
                {
                    minRefulerDistance = distSquare;
                    closestFuelSoFar   = pump;
                }
            }

            return(closestFuelSoFar);
        }
コード例 #10
0
ファイル: BattleEngineTest.cs プロジェクト: ekolis/stars-nova
        public void Test6DoBattle()
        {
            Fleet        stackA         = zoneStacks[0] as Fleet;
            Fleet        stackB         = zoneStacks[1] as Fleet;
            List <Fleet> battlingFleets = new List <Fleet>();

            battlingFleets.Add(stackA);
            battlingFleets.Add(stackB);

            double distanceS, distanceE;

            distanceS = PointUtilities.Distance(stackA.Position, stackB.Position);

            battleEngine.DoBattle(zoneStacks);

            distanceE = PointUtilities.Distance(stackA.Position, stackB.Position);

            // Check that the fleets have moved towards each other.

            Assert.Greater(distanceS, distanceE);
        }
コード例 #11
0
ファイル: FleetDetail.cs プロジェクト: ekolis/stars-nova
        /// <Summary>
        /// If there is another waypoint before the selected one, display the fuel,
        /// time, etc. required for this leg.
        /// </Summary>
        /// <param name="index">Index of the waypoint to display.</param>
        private void DisplayLegDetails(int index)
        {
            Waypoint thisWaypoint = selectedFleet.Waypoints[index];

            WaypointTasks.Text = thisWaypoint.Task.Name;

            if (selectedFleet.Waypoints.Count == 1)
            {
                thisWaypoint.WarpFactor = 0;
            }

            selectedFleet.Waypoints[index] = thisWaypoint;
            warpFactor.Value = thisWaypoint.WarpFactor;
            warpText.Text    = "Warp " + thisWaypoint.WarpFactor;

            if (index > 0 && thisWaypoint.WarpFactor > 0)
            {
                Waypoint from     = selectedFleet.Waypoints[index - 1];
                Waypoint to       = selectedFleet.Waypoints[index];
                double   distance = PointUtilities.Distance(from.Position, to.Position);

                double time = distance / (to.WarpFactor * to.WarpFactor);

                double fuelUsed = selectedFleet.FuelConsumption(to.WarpFactor, empireState.Race)

                                  * time;

                legDistance.Text = string.Format("{0}", distance.ToString("f1"));
                legFuel.Text     = string.Format("{0}", fuelUsed.ToString("f1"));
                legTime.Text     = string.Format("{0}", time.ToString("f1"));
            }
            else
            {
                legDistance.Text = "0";
                legFuel.Text     = "0";
                legTime.Text     = "0";
            }

            Waypoint previous     = null;
            double   fuelRequired = 0;

            // Sum up the total fuel required for all waypoints in the current
            // route (as long as there is more than one waypoint).

            foreach (Waypoint waypoint in selectedFleet.Waypoints)
            {
                if (previous != null && waypoint.WarpFactor > 0)
                {
                    double distance   = PointUtilities.Distance(waypoint.Position, previous.Position);
                    int    warp       = waypoint.WarpFactor;
                    double speed      = warp * warp;
                    double travelTime = distance / speed;

                    fuelRequired += selectedFleet.FuelConsumption(warp, empireState.Race) * travelTime;
                }
                previous = waypoint;
            }

            System.Drawing.Color color = fuelRequired > selectedFleet.FuelAvailable ? System.Drawing.Color.Red : System.Drawing.Color.Black;
            routeFuelUse.ForeColor = color;
            label3.ForeColor       = color;
            label5.ForeColor       = color;

            routeFuelUse.Text = fuelRequired.ToString("f1");
        }
コード例 #12
0
ファイル: StarMap.cs プロジェクト: ekolis/stars-nova
        /// <Summary>
        /// Build a list of all Minefields that are visible to the player.
        /// </Summary>
        /// <remarks>
        /// This consists
        /// of:
        ///
        /// (1) Minefields owned by the player
        /// (2) Minefiels within the range of scanners on ships owned by the player
        /// (3) Minefields within the range of scanners on planets owned by the player
        /// </remarks>
        private void DetermineVisibleMinefields()
        {
            List <Fleet> playersFleets = new List <Fleet>();

            foreach (Fleet fleet in clientState.EmpireState.OwnedFleets.Values)
            {
                if (fleet.Owner == clientState.EmpireState.Id)
                {
                    playersFleets.Add(fleet);
                }
            }

            // -------------------------------------------------------------------
            // (1) First the easy one. Minefields owned by the player.
            // -------------------------------------------------------------------

            foreach (Minefield minefield in this.turnData.AllMinefields.Values)
            {
                if (minefield.Owner == clientState.EmpireState.Id)
                {
                    this.visibleMinefields[minefield.Key] = minefield;
                }
            }

            // -------------------------------------------------------------------
            // (2) Not so easy. Minefields within the scanning range of the
            // player's ships.
            // -------------------------------------------------------------------

            foreach (Fleet fleet in playersFleets)
            {
                foreach (Minefield minefield in this.turnData.AllMinefields.Values)
                {
                    bool isIn = PointUtilities.CirclesOverlap(
                        fleet.Position,
                        minefield.Position,
                        fleet.ScanRange,
                        minefield.Radius);

                    if (isIn == true)
                    {
                        this.visibleMinefields[minefield.Key] = minefield;
                    }
                }
            }

            // -------------------------------------------------------------------
            // (3) Now that we know how to deal with ship scanners planet scanners
            // are just the same.
            // -------------------------------------------------------------------

            foreach (Minefield minefield in turnData.AllMinefields.Values)
            {
                foreach (Star report in clientState.EmpireState.OwnedStars.Values)
                {
                    if (report.Owner == clientState.EmpireState.Id)
                    {
                        bool isIn = PointUtilities.CirclesOverlap(
                            report.Position,
                            minefield.Position,
                            report.ScanRange,
                            minefield.Radius);

                        if (isIn == true)
                        {
                            this.visibleMinefields[minefield.Key] = minefield;
                        }
                    }
                }
            }
        }
コード例 #13
0
ファイル: FleetReport.cs プロジェクト: ekolis/stars-nova
        /// ----------------------------------------------------------------------------
        /// <Summary>
        /// Populate the display. We use unbound population because some of the fields
        /// need a little logic to decode (we don't just have a bunch of strings).
        /// </Summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">A <see cref="EventArgs"/> that contains the event data.</param>
        /// ----------------------------------------------------------------------------
        private void OnLoad(object sender, EventArgs e)
        {
            const int NumColumns = 11;

            this.fleetGridView.Columns[6].Name = "Cargo";
            this.fleetGridView.AutoSize        = true;

            foreach (Fleet fleet in empireState.OwnedFleets.Values)
            {
                if (fleet.Owner == empireState.Id)
                {
                    if (fleet.Type == ItemType.Starbase)
                    {
                        continue;
                    }

                    string location;
                    if (fleet.InOrbit != null)
                    {
                        location = fleet.InOrbit.Name;
                    }
                    else
                    {
                        location = "Space at " + fleet.Position.ToString();
                    }

                    string destination = "-";
                    string eta         = "-";
                    string task        = "-";

                    if (fleet.Waypoints.Count > 1)
                    {
                        Waypoint waypoint = fleet.Waypoints[1];

                        destination = waypoint.Destination;
                        if (!(waypoint.Task is NoTask))
                        {
                            task = waypoint.Task.Name;
                        }

                        double distance = PointUtilities.Distance(
                            waypoint.Position, fleet.Position);

                        double speed = waypoint.WarpFactor * waypoint.WarpFactor;
                        double time  = distance / speed;

                        eta = time.ToString("F1");
                    }

                    Cargo         cargo     = fleet.Cargo;
                    StringBuilder cargoText = new StringBuilder();

                    cargoText.AppendFormat(
                        "{0} {1} {2} {3}",
                        cargo.Ironium,
                        cargo.Boranium,
                        cargo.Germanium,
                        cargo.ColonistsInKilotons);

                    int      i   = 0;
                    string[] row = new string[NumColumns];

                    row[i++] = fleet.Name;
                    row[i++] = location;
                    row[i++] = destination;
                    row[i++] = eta;
                    row[i++] = task;
                    row[i++] = fleet.FuelAvailable.ToString("f1");
                    row[i++] = cargoText.ToString();
                    row[i++] = fleet.Composition.Count.ToString(System.Globalization.CultureInfo.InvariantCulture);
                    row[i++] = "-";
                    row[i++] = fleet.BattlePlan;
                    row[i++] = fleet.Mass.ToString(System.Globalization.CultureInfo.InvariantCulture);

                    this.fleetGridView.Rows.Add(row);
                }
            }

            this.fleetGridView.AutoResizeColumns();
        }
コード例 #14
0
        /// <summary>
        /// Move stacks towards their targets (if any). Record each movement in the
        /// battle report.
        /// </summary>
        /// <param name="battlingStacks">All stacks in the battle.</param>
        public void MoveStacks(List <Stack> battlingStacks)
        {
            // Movement in Squares per Round
            //                  Round
            // Movement  1  2  3  4  5  6  7  8
            // 1/2       1  0  1  0  1  0  1  0
            // 3/4       1  1  0  1  1  1  0  1
            // 1         1  1  1  1  1  1  1  1
            // 1 1/4     2  1  1  1  2  1  1  1
            // 1 1/2     2  1  2  1  2  1  2  1
            // 1 3/4     2  2  1  2  2  2  1  2
            // 2         2  2  2  2  2  2  2  2
            // 2 1/4     3  2  2  2  3  2  2  2
            // 2 1/2     3  2  3  2  3  2  3  2
            // repeats for rounds 9 - 16

            // In Stars! each round breaks movement into 3 phases.
            // Phase 1: All stacks that can move 3 squares this round get to move 1 square.
            // Phase 2: All stacks that can move 2 or more squares this round get to move 1 square.
            // Phase 3: All stacks that can move this round get to move 1 square.
            // TODO (priority 3) - verify that a ship should be able to move 1 square per phase if it has 3 move points, or is it limited to 1 per turn?
            for (var phase = 1; phase <= movementPhasesPerRound; phase++)
            {
                // TODO (priority 5) - Move in order of ship mass, juggle by 15%
                foreach (Stack stack in battlingStacks)
                {
                    if (stack.Target != null & !stack.IsStarbase)
                    {
                        NovaPoint from = stack.Position;
                        NovaPoint to   = stack.Target.Position;

                        int movesThisRound = 1; // FIXME (priority 6) - kludge until I implement the above table
                        if (stack.BattleSpeed <= 0.5)
                        {
                            movesThisRound = movementTable[0, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 0.75)
                        {
                            movesThisRound = movementTable[1, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 1.0)
                        {
                            movesThisRound = movementTable[2, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 1.25)
                        {
                            movesThisRound = movementTable[3, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 1.5)
                        {
                            movesThisRound = movementTable[4, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 1.75)
                        {
                            movesThisRound = movementTable[5, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 2.0)
                        {
                            movesThisRound = movementTable[6, battleRound % 8];
                        }
                        else if (stack.BattleSpeed <= 2.25)
                        {
                            movesThisRound = movementTable[7, battleRound % 8];
                        }
                        else
                        {
                            // stack.BattleSpeed > 2.25
                            movesThisRound = movementTable[8, battleRound % 8];
                        }

                        bool moveThisPhase = true;
                        switch (phase)
                        {
                        case 1:
                        {
                            moveThisPhase = movesThisRound == 3;
                            break;
                        }

                        case 2:
                        {
                            moveThisPhase = movesThisRound >= 2;
                            break;
                        }

                        case 3:
                        {
                            moveThisPhase = movesThisRound >= 1;
                            break;
                        }
                        }

                        // stack can move only after accumulating at least 1 move point, and after doing so expends that 1 move point
                        if (moveThisPhase)
                        {
                            stack.Position = PointUtilities.BattleMoveTo(from, to);

                            // Update the battle report with these movements.
                            BattleStepMovement report = new BattleStepMovement();
                            report.StackKey = stack.Key;
                            report.Position = stack.Position;
                            battle.Steps.Add(report);
                        }
                    }
                    // TODO (priority 7) - shouldn't stacks without targets flee the battle if their strategy says to do so? they're sitting ducks now!
                }
            }
        }
コード例 #15
0
ファイル: ScanStep.cs プロジェクト: ekolis/stars-nova
        private void Scan(EmpireData empire)
        {
            foreach (Mappable scanner in empire.IterateAllMappables())
            {
                int scanRange    = 0;
                int penScanRange = 0;

                //Do some self scanning (Update reports) and set ranges..
                if (scanner is Star)
                {
                    scanRange    = (scanner as Star).ScanRange;
                    penScanRange = (scanner as Star).ScanRange; // TODO:(priority 6) Planetary Pen-Scan not implemented yet.
                    empire.StarReports[scanner.Name].Update(scanner as Star, ScanLevel.Owned, serverState.TurnYear);
                }
                else
                {
                    scanRange    = (scanner as Fleet).ScanRange;
                    penScanRange = (scanner as Fleet).PenScanRange;
                    empire.FleetReports[scanner.Key].Update(scanner as Fleet, ScanLevel.Owned, empire.TurnYear);
                }

                // Scan everything
                foreach (Mappable scanned in serverState.IterateAllMappables())
                {
                    // ...That isn't ours!
                    if (scanned.Owner == empire.Id)
                    {
                        continue;
                    }

                    ScanLevel scanLevel = ScanLevel.None;
                    double    range     = 0;
                    range = PointUtilities.Distance(scanner.Position, scanned.Position);

                    if (scanned is Star)
                    {
                        Star star = scanned as Star;

                        // There are two ways to get information from a Star:
                        // 1. In orbit with a fleet,
                        // 2. Not in orbit with a Pen Scan (fleet or star).
                        // Non penetrating distance scans won't tell anything about it.
                        if ((scanner is Fleet) && range == 0)
                        {
                            scanLevel = (scanner as Fleet).CanScan ? ScanLevel.InDeepScan : ScanLevel.InPlace;
                        }
                        else // scanner is Star or non orbiting Fleet
                        {
                            scanLevel = (range <= penScanRange) ? ScanLevel.InDeepScan : ScanLevel.None;
                        }

                        // Dont update if we didn't scan to allow report to age.
                        if (scanLevel == ScanLevel.None)
                        {
                            continue;
                        }

                        if (empire.StarReports.ContainsKey(scanned.Name))
                        {
                            empire.StarReports[scanned.Name].Update((scanned as Star), scanLevel, serverState.TurnYear);
                        }
                        else
                        {
                            empire.StarReports.Add(scanned.Name, (scanned as Star).GenerateReport(scanLevel, serverState.TurnYear));
                        }
                    }
                    else // scanned is Fleet
                    {
                        // Fleets are simple as scan levels (PenScan for example) won't affect them. We only
                        // care for non penetrating distance scans.
                        if (range > scanRange)
                        {
                            continue;
                        }

                        // Check if we have a record of this design(s).
                        foreach (ShipToken token in (scanned as Fleet).Composition.Values)
                        {
                            if (empire.EmpireReports[scanned.Owner].Designs.ContainsKey(token.Design.Key))
                            {
                                continue;
                            }

                            // If not, add just the empty Hull.
                            ShipDesign newDesign = new ShipDesign(token.Design);
                            newDesign.Key = token.Design.Key;
                            newDesign.ClearAllocated();
                            empire.EmpireReports[scanned.Owner].Designs.Add(newDesign.Key, newDesign);
                        }

                        if (!empire.FleetReports.ContainsKey(scanned.Key))
                        {
                            empire.FleetReports.Add(scanned.Key, (scanned as Fleet).GenerateReport(ScanLevel.InScan, serverState.TurnYear));
                        }
                        else
                        {
                            empire.FleetReports[scanned.Key].Update((scanned as Fleet), ScanLevel.InScan, serverState.TurnYear);
                        }
                    }
                }
            }
        }
コード例 #16
0
        private IReadOnlyList <string> GetRoute(IReadOnlyDictionary <Point, char> view)
        {
            const char scaffold = '#';

            var route = new List <string>();

            var currentDirection = Direction.Up;
            var currentLocation  = view.Single(pixel => pixel.Value == '^').Key;

            var keepGoing = true;
            var length    = 0;

            while (keepGoing)
            {
                var lookAheadLocation = GetLookAheadLocation(currentLocation, currentDirection);
                view.TryGetValue(lookAheadLocation, out var lookAheadPixel);

                if (lookAheadPixel == scaffold)
                {
                    // Keep going in same direction
                    length++;

                    currentLocation = currentDirection.Id switch
                    {
                        'U' => PointUtilities.MoveUp(currentLocation),
                        'R' => PointUtilities.MoveRight(currentLocation),
                        'D' => PointUtilities.MoveDown(currentLocation),
                        'L' => PointUtilities.MoveLeft(currentLocation),
                        _ => throw new ArgumentOutOfRangeException()
                    };
                }
                else
                {
                    lookAheadLocation = GetLookAheadLocation(currentLocation, currentDirection.TurnRight);
                    view.TryGetValue(lookAheadLocation, out lookAheadPixel);

                    if (lookAheadPixel == scaffold)
                    {
                        // Scaffold on the right
                        if (length > 0)
                        {
                            route.Add(length.ToString());
                        }

                        route.Add(Direction.Right.Id.ToString());
                        currentDirection = currentDirection.TurnRight;
                        length           = 0;
                    }
                    else
                    {
                        lookAheadLocation = GetLookAheadLocation(currentLocation, currentDirection.TurnLeft);
                        view.TryGetValue(lookAheadLocation, out lookAheadPixel);

                        if (lookAheadPixel == scaffold)
                        {
                            // Scaffold on the left
                            if (length > 0)
                            {
                                route.Add(length.ToString());
                            }

                            route.Add(Direction.Left.Id.ToString());
                            currentDirection = currentDirection.TurnLeft;
                            length           = 0;
                        }
                        else
                        {
                            // Scaffold only behind so must have reached the end
                            if (length > 0)
                            {
                                route.Add(length.ToString());
                            }

                            keepGoing = false;
                        }
                    }
                }
            }

            return(route);
        }