6

First of all I have to specify that I'm working in Unity 5.3 and the new MonoDevelop doesn't allow me to debug. Unity just crashes :(

So I have a list of "goals" that I need to sort based on 3 criterias:

  1. first should be listed the "active" goals
  2. then ordered by difficulty level
  3. finally randomly for goals of the same level

here is a my code:

public class Goal {
    public int ID;
    public int Level;
    public bool Active;
}

...

List<Goal> goals;

goals.Sort((a, b) => {
    // first chooses the Active ones (if any)
    var sort = b.Active.CompareTo(a.Active);
    if (sort == 0) {
        // then sort by level
        sort = (a.Level).CompareTo(b.Level);
        // if same level, randomize. Returns -1, 0 or 1
        return sort == 0 ? UnityEngine.Random.Range(-1, 2) : sort;
    } else {
        return sort;
    }
});

When I run this code sometimes I get one or more active goals after inactive ones, but I don't understand why.

mcmorry
  • 132
  • 1
  • 7
  • 2
    Can you narrow the behavior to a specific set of inputs? – Code-Apprentice Jan 29 '16 at 19:57
  • The problem likely has to do with the sort order not being unique. That is: any given order *might* be correct, but due to the randomization of same-priority items, any attempt to verify this order will return a different order. I would find a different way of shuffling things – Draco18s no longer trusts SE Jan 29 '16 at 19:59
  • Can you give a sample input that reproduces (or has a chance of reproducing) the issue? – Yacoub Massad Jan 29 '16 at 20:17
  • Do you have concurrent code that changes the Active property of existing Goals? If yes, then I would guess that the Active property of goals is changed during or even after the sort. Also: Maybe goals are added after the sorting. – NineBerry Jan 29 '16 at 20:24
  • @NineBerry I don't think that this is the case because I'm not using additional threads. – mcmorry Jan 29 '16 at 20:27
  • Your code looks fairly straightforward and functional to me. As @NineBerry mentioned, the culprit behind the disordering is probably elsewhere. Possibly values being changed just before or after this sorting? – andeart Jan 29 '16 at 20:28
  • @Draco18s yes this is was my same thought, but I was expecting that at least the Active goals should not have problems to be sorted because that is the first rule and is predictable. – mcmorry Jan 29 '16 at 20:28
  • Remove the random element: does it still crash? – Draco18s no longer trusts SE Jan 29 '16 at 20:29
  • Maybe you don't have separate threads but the Unity engine has. Or the Active property is changed after the sorting and before the items are displayed in the user interface. If you can't work with the debugger, you will have to use logging to retrace what is happening – NineBerry Jan 29 '16 at 20:32
  • I'm logging in the console just after the sorting. There should be something hidden elsewhere. I'll investigate more. I'll also try to remove the random sorting. Thanks for all the hints. – mcmorry Jan 29 '16 at 20:35
  • Add logging to the places where the Active property is changed or new items are added to the list. Then see whether anything of this happens during the Sorting. – NineBerry Jan 29 '16 at 20:37
  • 1
    @mcmorry Do let us know what it was please :) – andeart Jan 29 '16 at 21:02
  • @andeart sure I'm digging... It's night for me. I'll have to continue tomorrow, but I'll report back for sure. – mcmorry Jan 29 '16 at 21:08
  • On another note. Visual studio works very well with Unity and has an amazing debugger :) – acid1789 Jan 29 '16 at 22:15
  • @mcmorry FWIW, you could just `return sort;` instead of the randomisation line (seeing as there's so many posts trying to fault that out). Since it does not matter to you how values are ordered if they have both the same active and level, this change in code would just leave those values relatively ordered the same as they already are. – andeart Jan 29 '16 at 22:52
  • So after many tests it seems that sorting by random values was scrambling the data. When I just sort by active and level it works. Would be interesting to understand why anyway... Instead, the second option proposed by alexm, works without problems, because is shuffling inside each level. Is it a Mono bug? – mcmorry Jan 30 '16 at 11:26

3 Answers3

3

To work correctly a sorting algorithm should not depend on mutating state. Explanation of why using a random generator while comparing values is not a good idea is given here.

The problem can be solved in two ways:

Option 1: Pre-compute random numbers

    var tmp = goals.Select( g=> new {goal = g, weight = rnd.NextDouble()})
        .OrderByDescending(t=>t.goal.Active) // Active first
        .ThenBy(t=>t.goal.Level)
        .ThenBy(t=>t.weight)
        .Select(t=>t.goal)
        .ToList();



    goals.Clear();
    goals.AddRange(tmp);

Working sample

Option 2: Sort then shuffle ties

Random rnd = new Random();

Comparison<Goal> comparison =  (a, b) => {
// first chooses the Active ones (if any)
var sort = b.Active.CompareTo(a.Active);

if (sort == 0) {
// then sort by level
    return sort = (a.Level).CompareTo(b.Level);
  } else 
{
   return sort;
}
};



int startIndex = 0;
int endIndex = 0;

goals.Sort(comparison);

while (startIndex < goals.Count)
{
    for (endIndex = startIndex + 1; endIndex < goals.Count; ++endIndex)
    {
       if (comparison(goals[startIndex], goals[endIndex]) != 0)
       {
          //End of tie
          break;
       }
    }

    if (endIndex - startIndex > 1)
    {
       // Shuffle goals of the same level
       ShuffleRange(goals, startIndex, endIndex - startIndex, rnd);
    }

    startIndex = endIndex;
}   

static void ShuffleRange<T>(List<T> list, int startIndex, int count, Random rnd)
{
     int n = startIndex + count;  
     while (n > startIndex + 1) 
     {  
        int k = rnd.Next(startIndex, n--);  
        T value = list[k];  
        list[k] = list[n];  
        list[n] = value;  
    }  
}           

Working sample

Shuffle algorithm is borrowed from here

Community
  • 1
  • 1
alexm
  • 6,854
  • 20
  • 24
  • OP's randomisation is only among the sorting of the deeper elements (after they've been assigned the right order based on active and priority against other priority levels), where it does not matter. No? – andeart Jan 29 '16 at 22:44
  • Please also try OP's code. It works as OP intends- the order of the random-ordered values among themselves are irrelevant. – andeart Jan 29 '16 at 22:49
  • @andeart: OP states in the end that code does not _always_ work correctly. – alexm Jan 29 '16 at 22:55
  • ...which is what OP thinks is the issue. Are you stating that the code that OP posted has potential error(s) that will NOT give the result that OP wants? As some others have discussed in the comments directly on the question, the code correctly gives the results that OP wants and that the issue might not be there at all (as OP might have thought while posting). – andeart Jan 29 '16 at 22:58
  • Just being analytical ;) – andeart Jan 29 '16 at 23:05
  • I don't know why when I test my code on dotnetfiddle it works, but inside Unity not. Infact when I tried to disable the random sorting it was ok. It seems that the Mono implementation of the Sort method scramble the whole array... That is so weird. But with your solution (I prefer the second one to avoid additional memory allocation) it works very well. – mcmorry Jan 30 '16 at 11:21
0

Try this lambda:

(a, b) => ((b.Active ? 1000 : 0) + b.Level) - ((a.Active ? 1000 : 0) + a.Level)

Active is 1000 times more important than a level difference of 1. This works for up to 1000 levels. When Active is the same, the level becomes relevant. Finally, if it still is the same it will be ordered in a deterministic but irrelevant manner, which is the same as random.

There's no need to use a true random number. The order will always be the same during one run, but it may differ between runs. If you really need a random order you can use this:

(a, b) =>
    ((b.Active ? 10000 : 0) + b.Level * 10) -
    ((a.Active ? 10000 : 0) + a.Level * 10) + UnityEngine.Random.Range(-1, 2)
pid
  • 11,472
  • 6
  • 34
  • 63
0

I can't reproduce your issue of getting "one or more active goals after inactive ones". It sounds like your Goal instances are being mutated after the sort. I would suggest trying to make read-only objects where possible.

My other suggestion is to simplify you sort code to make it clearer and easier to reason about - although that's probably not going to directly help in this case.

Just do this sort:

var sorted =
(
    from g in goals
    orderby g.Active descending, g.Level, UnityEngine.Random.Range(-1, 2)
    select g
.ToList();

...or alternatively and equivalently like this:

var sorted =
    goals
        .OrderByDescending(g => g.Active)
        .ThenBy(g => g.Level)
        .ThenBy(g => rnd.Next())
        .ToList();

LINQ sorts work fine with a random source. I've done the distribution analysis on it and it works just as well as fisher-yates.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172