From 24d733f273bc457b81a15f03ece028d1c7c2efbb Mon Sep 17 00:00:00 2001
From: Svornost <11434-svornost@users.noreply.gitgud.io>
Date: Fri, 31 Jul 2020 15:55:12 -0700
Subject: [PATCH] Add cycle detection to inbreeding calculation.

---
 src/js/ibcJS.js | 40 ++++++++++++++++++++++++----------------
 1 file changed, 24 insertions(+), 16 deletions(-)

diff --git a/src/js/ibcJS.js b/src/js/ibcJS.js
index de19d724130..c0330d7f151 100644
--- a/src/js/ibcJS.js
+++ b/src/js/ibcJS.js
@@ -182,30 +182,30 @@ globalThis.ibc = (() => {
         return n._coeff;
     };
 
-    // Populate the NodeCodes. 
+    // Populate the NodeCodes.
     //
     // Each node has a set of NodeCodes, which represent the set of paths from it to its ancestors.
     // NodeCodes here are represented by arrays of integers.
     //
     // NodeCodes are constructed recursively in this fashion:
     //
-    // - Assign each of the founders (nodes with both parents === null) an unique ID, starting from 
-    //   0 and incrementing each time (the order doesn't matter); a founder's set of NodeCodes has 
+    // - Assign each of the founders (nodes with both parents === null) an unique ID, starting from
+    //   0 and incrementing each time (the order doesn't matter); a founder's set of NodeCodes has
     //   exactly one NodeCode, which is [ID] (an array containing only their ID)
     //
-    // - For each other node, let M be its child number w.r.t. its mother and N its child number 
-    //   w.r.t. its father, i.e. the number of children that the respective parent has had before 
-    //   this one (the order is not important to the algorithm, it's arbitrary here for 
+    // - For each other node, let M be its child number w.r.t. its mother and N its child number
+    //   w.r.t. its father, i.e. the number of children that the respective parent has had before
+    //   this one (the order is not important to the algorithm, it's arbitrary here for
     //   convenience). Its set of NodeCodes is the set of all its mother's NodeCodes with M appended
-    //   and all of its father's NodeCodes with N appended. For example, if its mother has the 
-    //   NodeCodes [[2]] and M = 3 and its father has the NodeCodes [[0,1], [3,1]] and N = 1 then 
+    //   and all of its father's NodeCodes with N appended. For example, if its mother has the
+    //   NodeCodes [[2]] and M = 3 and its father has the NodeCodes [[0,1], [3,1]] and N = 1 then
     //   the set of NodeCodes for this node would be
-    //       
+    //
     //       [[2, 3], [0, 1, 1], [3, 1, 1]]
     //
     // We do this iteratively here, looping over the set of all nodes until each has been assigned
-    // a NodeCode. This requires looping through a number of times equal to the number of 
-    // generations, since as soon as both parents have NodeCodes their children's NodeCodes may be 
+    // a NodeCode. This requires looping through a number of times equal to the number of
+    // generations, since as soon as both parents have NodeCodes their children's NodeCodes may be
     // computed.
     let make_nc = nodes => {
         // Generate founder NodeCodes
@@ -221,7 +221,9 @@ globalThis.ibc = (() => {
         });
 
         // Generate the rest of the NodeCodes
-        while (seen.length != total) {
+        let oldSeenLength = -1;
+        while (seen.length !== total) {
+            oldSeenLength = seen.length;
             Object.keys(nodes).forEach(s=> {
                 let n = nodes[s];
                 if (seen.includes(+s)) // We've already done this
@@ -232,7 +234,7 @@ globalThis.ibc = (() => {
                 seen.push(n.id);
                 // Compute the NodeCodes from its parents
                 [n.mother, n.father].forEach((a, i) => {
-                    if (a === null || (n.mother === n.father && i == 1)) // Ignore missing parents/repeated
+                    if (a === null || (n.mother === n.father && i === 1)) // Ignore missing parents/repeated
                         return;
 
                     a.nodecodes.forEach(nc => {
@@ -245,8 +247,14 @@ globalThis.ibc = (() => {
                 });
 
                 // NodeCodes must be sorted; this suffices
-                n.nodecodes.sort()
+                n.nodecodes.sort();
             });
+            // check to make sure we're progressing...if not, there's a cycle in the graph
+            // dump all the nodes participating in or descended from the cycle and let the player figure it out
+            if (oldSeenLength === seen.length) {
+                const badSlaveIDs = Object.keys(nodes).filter(s => !seen.includes(+s)).map(k => nodes[k].id);
+                throw(`Inbreeding calculation: heritance cycle detected. Check slave IDs: ${badSlaveIDs}`);
+            }
         }
 
         // Cache the string NodeCodes
@@ -261,7 +269,7 @@ globalThis.ibc = (() => {
 
         // Recursively create the nodes we need, moving upwards from the given slave
         let create_node_rec = s => {
-            // Certain parents (e.g. 0, societal elite) are not considered to be related, despite 
+            // Certain parents (e.g. 0, societal elite) are not considered to be related, despite
             // having the same ID; convert them to null
             let m = or_null(s.mother);
             let f = or_null(s.father);
@@ -269,7 +277,7 @@ globalThis.ibc = (() => {
             // Ensure that parent nodes are created
             [m, f].forEach(p => {
                 if (p !== null && !(p in nodes)) { // Not created, we have to do something
-                    if (p === -1) { 
+                    if (p === -1) {
                         create_node_rec(SugarCube.State.variables.PC);
                     } else {
                         // Search for a slave state, genePool entry, or missingTable entry
-- 
GitLab