Fix issues preventing suboptimal path searches.

Two different issues were causing a path search to not explore cells in order of the cheapest estimated route first. This meant the search could sometimes miss a cheaper route and return a suboptimal path.

- PriorityQueue had a bug which would cause it to not check some elements when restoring the heap property of its internal data structure. Failing to do this would invalidate the heap property, meaning it would not longer return the items in correct priority order. Additional tests ensure this is covered.
- When a path search encountered the same cell again with a lower cost, it would not update the priority queue with the new cost. This meant the cell was not explored early enough as it was in the queue with its original, higher cost. Exploring other paths might close off surrounding cells, preventing the cell with the lower cost from progressing. Instead we now add a duplicate with the lower cost to ensure it gets explored at the right time. We remove the duplicate with the higher cost in CanExpand by checking for already Closed cells.
This commit is contained in:
RoosterDragon
2022-05-08 21:36:13 +01:00
committed by abcdefg30
parent c1cb9ea6be
commit c9ee902510
3 changed files with 130 additions and 11 deletions

View File

@@ -10,30 +10,133 @@
#endregion
using System;
using System.Linq;
using NUnit.Framework;
using OpenRA.Mods.Common;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Test
{
[TestFixture]
class PriorityQueueTest
{
[TestCase(TestName = "PriorityQueue maintains invariants when adding and removing items.")]
public void PriorityQueueGeneralTest()
[TestCase(1, 123)]
[TestCase(1, 1234)]
[TestCase(1, 12345)]
[TestCase(2, 123)]
[TestCase(2, 1234)]
[TestCase(2, 12345)]
[TestCase(10, 123)]
[TestCase(10, 1234)]
[TestCase(10, 12345)]
[TestCase(15, 123)]
[TestCase(15, 1234)]
[TestCase(15, 12345)]
[TestCase(16, 123)]
[TestCase(16, 1234)]
[TestCase(16, 12345)]
[TestCase(17, 123)]
[TestCase(17, 1234)]
[TestCase(17, 12345)]
[TestCase(100, 123)]
[TestCase(100, 1234)]
[TestCase(100, 12345)]
[TestCase(1000, 123)]
[TestCase(1000, 1234)]
[TestCase(1000, 12345)]
public void PriorityQueueAddThenRemoveTest(int count, int seed)
{
var mt = new MersenneTwister(seed);
var values = Enumerable.Range(0, count);
var shuffledValues = values.Shuffle(mt).ToArray();
var queue = new PriorityQueue<int>();
Assert.IsTrue(queue.Empty, "New queue should start out empty.");
Assert.Throws<InvalidOperationException>(() => queue.Peek(), "Peeking at an empty queue should throw.");
Assert.Throws<InvalidOperationException>(() => queue.Pop(), "Popping an empty queue should throw.");
foreach (var value in new[] { 4, 3, 5, 1, 2 })
foreach (var value in shuffledValues)
{
queue.Add(value);
Assert.IsFalse(queue.Empty, "Queue should not be empty - items have been added.");
}
foreach (var value in new[] { 1, 2, 3, 4, 5 })
foreach (var value in values)
{
Assert.AreEqual(value, queue.Peek(), "Peek returned the wrong item - should be in order.");
Assert.IsFalse(queue.Empty, "Queue should not be empty yet.");
Assert.AreEqual(value, queue.Pop(), "Pop returned the wrong item - should be in order.");
}
Assert.IsTrue(queue.Empty, "Queue should now be empty.");
Assert.Throws<InvalidOperationException>(() => queue.Peek(), "Peeking at an empty queue should throw.");
Assert.Throws<InvalidOperationException>(() => queue.Pop(), "Popping an empty queue should throw.");
}
[TestCase(15, 123)]
[TestCase(15, 1234)]
[TestCase(15, 12345)]
[TestCase(16, 123)]
[TestCase(16, 1234)]
[TestCase(16, 12345)]
[TestCase(17, 123)]
[TestCase(17, 1234)]
[TestCase(17, 12345)]
[TestCase(100, 123)]
[TestCase(100, 1234)]
[TestCase(100, 12345)]
[TestCase(1000, 123)]
[TestCase(1000, 1234)]
[TestCase(1000, 12345)]
public void PriorityQueueAddAndRemoveInterleavedTest(int count, int seed)
{
var mt = new MersenneTwister(seed);
var shuffledValues = Enumerable.Range(0, count).Shuffle(mt).ToArray();
var queue = new PriorityQueue<int>();
Assert.IsTrue(queue.Empty, "New queue should start out empty.");
Assert.Throws<InvalidOperationException>(() => queue.Peek(), "Peeking at an empty queue should throw.");
Assert.Throws<InvalidOperationException>(() => queue.Pop(), "Popping an empty queue should throw.");
foreach (var value in shuffledValues.Take(10))
{
queue.Add(value);
Assert.IsFalse(queue.Empty, "Queue should not be empty - items have been added.");
}
foreach (var value in shuffledValues.Take(10).OrderBy(x => x).Take(5))
{
Assert.AreEqual(value, queue.Peek(), "Peek returned the wrong item - should be in order.");
Assert.IsFalse(queue.Empty, "Queue should not be empty yet.");
Assert.AreEqual(value, queue.Pop(), "Pop returned the wrong item - should be in order.");
}
foreach (var value in shuffledValues.Skip(10).Take(5))
{
queue.Add(value);
Assert.IsFalse(queue.Empty, "Queue should not be empty - items have been added.");
}
foreach (var value in shuffledValues.Take(10).OrderBy(x => x).Skip(5)
.Concat(shuffledValues.Skip(10).Take(5)).OrderBy(x => x).Take(5))
{
Assert.AreEqual(value, queue.Peek(), "Peek returned the wrong item - should be in order.");
Assert.IsFalse(queue.Empty, "Queue should not be empty yet.");
Assert.AreEqual(value, queue.Pop(), "Pop returned the wrong item - should be in order.");
}
foreach (var value in shuffledValues.Skip(15))
{
queue.Add(value);
Assert.IsFalse(queue.Empty, "Queue should not be empty - items have been added.");
}
foreach (var value in shuffledValues.Take(10).OrderBy(x => x).Skip(5)
.Concat(shuffledValues.Skip(10).Take(5)).OrderBy(x => x).Skip(5)
.Concat(shuffledValues.Skip(15)).OrderBy(x => x))
{
Assert.AreEqual(value, queue.Peek(), "Peek returned the wrong item - should be in order.");
Assert.IsFalse(queue.Empty, "Queue should not be empty yet.");