From 5935da35536b90f852dba14c4f676bd57c6b64e0 Mon Sep 17 00:00:00 2001 From: Arkerthan <arkerthan@gmail.com> Date: Thu, 5 May 2022 13:09:45 +0200 Subject: [PATCH] Move ibc algorithm tests to tests/ --- src/js/ibcJS.js | 133 ++---------------------------------------------- tests/ibc.js | 131 +++++++++++++++++++++++++++++++++++++++++++++++ tests/index.js | 15 +++++- 3 files changed, 148 insertions(+), 131 deletions(-) create mode 100644 tests/ibc.js diff --git a/src/js/ibcJS.js b/src/js/ibcJS.js index 98b95030206..7ac1750f6a1 100644 --- a/src/js/ibcJS.js +++ b/src/js/ibcJS.js @@ -1,6 +1,3 @@ -const runningTestsWhenTheGameLoads = false; // set this to true to run some tests when loading the page. when they pass nothing happens, so to ensure it's working, - // you might tweak one of the expected values in the tests (as by changing 85/256 to 84/256, say) and then compile and reload the page. - /** @typedef IBCRelative * An very simple object that represents a entity in a family tree. * Represents a group of common properties shared by SlaveState, InfantState, and PlayerState, @@ -391,134 +388,10 @@ globalThis.ibc = (() => { return recalculate_coeff_ids(world, [id]); }; - if (runningTestsWhenTheGameLoads) { - class MockSlave - { - ID; - mother; - father; - - constructor(id) { - this.ID = id; - } - } - - class MockMating - { - constructor(fatherId, motherId, ...childrenIds) { - for (let id of childrenIds) - if (id === fatherId || id === motherId) - throw new Error("cannot give birth to self"); - - this.fatherId = fatherId; - this.motherId = motherId; - this.childrenIds = childrenIds; - } - } - - class MockWorld - { - slavesArray; - - constructor(matings) { - this.slavesArray = []; - - let slavesById = {}; - let meetSlave = (id) => { - if (!slavesById[id]) { - let slave = new MockSlave(id); - this.slavesArray.push(slave); - slavesById[id] = slave; - } - - return slavesById[id]; - }; - - for (let mating of matings) { - let father = meetSlave(mating.fatherId); - let mother = meetSlave(mating.motherId); - for (let id of mating.childrenIds) { - let child = meetSlave(id); - child.father = father.ID; - child.mother = mother.ID; - } - } - } - - findSlaveState(id) { - return this.slavesArray.find(slave => slave.ID === id) || null; - } - } - - let testCoefficientForSlave = function(mockMatings, slaveId, expectedCoefficientOfInbreeding) { - let tolerance = .00000000001; - - let world = new MockWorld(mockMatings); - let c = coeff_slave(world, world.findSlaveState(slaveId)); - if (typeof c !== "number" || Number.isNaN(c) || Math.abs(c - expectedCoefficientOfInbreeding) > tolerance) - throw new Error("slave " + slaveId + " had wrong coefficient - expected " + expectedCoefficientOfInbreeding + ", was " + c); - } - - // references: - // Introduction to Quantitative Genetics - Doulas S. Falconer, 1989 - // Genetic and Quantitative Aspects of Genealogy - F.M. Lancaster, 2015 - testCoefficientForSlave([new MockMating(1, 2, 3, 4)], 1, 0); // basic outbred mating - testCoefficientForSlave([new MockMating(1, 2, 3, 4)], 4, 0); - testCoefficientForSlave([new MockMating(1, 1, 3, 4)], 4, 1/2); // basic self-mating - testCoefficientForSlave([new MockMating(1, 1, 3, 4)], 1, 0); - testCoefficientForSlave([new MockMating(1, 2, 3), new MockMating(2, 3, 4)], 4, 1/4); // basic child-parent mating - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5)], 5, 1/4); // basic sibling mating - testCoefficientForSlave([new MockMating(1, 2, 3), new MockMating(2, 4, 5), new MockMating(3, 5, 6)], 6, 1/8); // basic half-sibling mating - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(5, 3, 6), new MockMating(4, 7, 8), new MockMating(6, 8, 9)], 9, 1/16); // basic first cousin mating - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8), - new MockMating(3, 7, 9), new MockMating(4, 8, 10), new MockMating(9, 10, 11)], 11, 1/8); // double first cousin mating - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(4, 5, 6), new MockMating(3, 6, 7)], 7, 1/8); // aunt-niece mating - let scenario53 = [new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8), new MockMating(3, 7, 9, 10), new MockMating(4, 8, 11), - new MockMating(12, 9, 13), new MockMating(10, 11, 14), new MockMating(13, 14, 15)]; - testCoefficientForSlave(scenario53, 14, 1/8); // problem 5.3 from - testCoefficientForSlave(scenario53, 15, 3/32); // Falconer - testCoefficientForSlave([new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 9), new MockMating(9, 10, 12, 13), - new MockMating(11, 12, 15), new MockMating(13, 14, 16), new MockMating(15, 16, 17, 18)], 17, 33/512); // example in figure 64 from Lancaster - testCoefficientForSlave([new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 10, 11), - new MockMating(9, 10, 13), new MockMating(11, 12, 14), new MockMating(13, 14, 15)], 15, 9/128); // example in figure 65 from Lancaster - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9)], 9, 1/2); // example in figure 66 from Lancaster - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11)], 11, 19/32); // extending the previous example by one more generation's worth of mating between siblings - // from here the preceding scenario of regular sibling-mating is extended by several more steps; by rules given in Falconer, the coefficient ck for step k, where step k follows - // step j which follows step i, should be ck = 1/4 + cj/2 + ci/4, and so that is how the expected values used in the next few tests were calculated - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13)], 13, 43/64); // 43/64 = 1/4 + (19/32) / 2 + (1/2) / 4 - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15)], 15, 94/128); - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17)], 17, 201/256); - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19)], 19, 423/512); - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), - new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19, 20), - new MockMating(19, 20, 21)], 21, 880/1024); - // as of this writing, the ten-generations-of-sisterfucking test immediately above is enough to occupy the game's coefficient-of-inbreeding calculation algorithm for a few minutes - // on a ~4Ghz CPU. this is very silly. when you read this, that algorithm should have been replaced by a much faster one, allowing the above test to be comfortably run again. - let scenario70 = [new MockMating(1, 2, 6), new MockMating(2, 3, 7), new MockMating(3, 4, 8), new MockMating(5, 6, 9), new MockMating(7, 8, 10), - new MockMating(9, 10, 12), new MockMating(10, 11, 13), new MockMating(12, 13, 14), new MockMating(12, 14, 15)]; // example in figure 70 from Lancaster - testCoefficientForSlave(scenario70, 2, 0); - testCoefficientForSlave(scenario70, 10, 1/8); - testCoefficientForSlave(scenario70, 12, 1/32); - testCoefficientForSlave(scenario70, 15, 85/256); - let scenarioXXX = [new MockMating(1, 2, 5, 6), new MockMating(3, 4, 7, 8), new MockMating(5, 6, 9, 10), new MockMating(5, 7, 11), new MockMating(5, 8, 12), - new MockMating(5, 9, 13), new MockMating(9, 10, 14), new MockMating(5, 11, 15), new MockMating(10, 12, 16), new MockMating(13, 16, 17), - new MockMating(4, 16, 18), new MockMating(14, 15, 19), new MockMating(6, 17, 20), new MockMating(6, 18, 21), new MockMating(6, 19, 22), - new MockMating(20, 21, 23), new MockMating(22, 23, 24)]; // an invented example meant to represent a typical situation in a respectable arcology - testCoefficientForSlave(scenarioXXX, 24, 633/2048); // here the expected coefficient was retrieved from the algorithm being tested and was not separately verified by hand, - // so this test could conceivably be wrong, but at least it can catch changes... - testCoefficientForSlave([new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), new MockMating(9, 10, 11, 12), - new MockMating(11, 12, 13), new MockMating(13, 14, 15), // an example in which the old and new algorithms actually gave different results - the first one such - new MockMating(5, 5, 16), new MockMating(15, 16, 17)], 17, 1/4); // to have been observed! it seems that the old algorithm was wrong - testCoefficientForSlave([new MockMating(-120, -120, 87)], 87, 1/2); // negative IDs need to be accepted (not bothering to test any weird special IDs like -1 here though) - testCoefficientForSlave([new MockMating(-999, -998, -300), new MockMating(-300, -998, 300)], 300, 1/4); - } - return { + _test: { + coeff_slave, + }, coeff: coeff_slave.bind(null, realWorld), coeff_slaves: coeff_slaves.bind(null, realWorld), kinship: kinship_slaves.bind(null, realWorld), diff --git a/tests/ibc.js b/tests/ibc.js new file mode 100644 index 00000000000..0199220e089 --- /dev/null +++ b/tests/ibc.js @@ -0,0 +1,131 @@ +{ + class MockSlave { + ID; + mother; + father; + + constructor(id) { + this.ID = id; + } + } + + class MockMating { + constructor(fatherId, motherId, ...childrenIds) { + for (let id of childrenIds) { + if (id === fatherId || id === motherId) { + throw new Error("cannot give birth to self"); + } + } + + this.fatherId = fatherId; + this.motherId = motherId; + this.childrenIds = childrenIds; + } + } + + class MockWorld { + slavesArray; + + constructor(matings) { + this.slavesArray = []; + + let slavesById = {}; + let meetSlave = (id) => { + if (!slavesById[id]) { + let slave = new MockSlave(id); + this.slavesArray.push(slave); + slavesById[id] = slave; + } + + return slavesById[id]; + }; + + for (let mating of matings) { + let father = meetSlave(mating.fatherId); + let mother = meetSlave(mating.motherId); + for (let id of mating.childrenIds) { + let child = meetSlave(id); + child.father = father.ID; + child.mother = mother.ID; + } + } + } + + findSlaveState(id) { + return this.slavesArray.find(slave => slave.ID === id) || null; + } + } + + let testCoefficientForSlave = function(name, mockMatings, slaveId, expectedCoefficientOfInbreeding) { + App.Testing.executeTest(name, () => {}, () => { + let tolerance = .00000000001; + + let world = new MockWorld(mockMatings); + let c = ibc._test.coeff_slave(world, world.findSlaveState(slaveId)); + App.Testing.isType(c, "number"); + App.Testing.notNaN(c); + App.Testing.inRange(c, expectedCoefficientOfInbreeding, tolerance); + }, () => {}); + }; + + // references: + // Introduction to Quantitative Genetics - Doulas S. Falconer, 1989 + // Genetic and Quantitative Aspects of Genealogy - F.M. Lancaster, 2015 + testCoefficientForSlave("basic outbred mating, 1",[new MockMating(1, 2, 3, 4)], 1, 0); + testCoefficientForSlave("basic outbred mating, 2",[new MockMating(1, 2, 3, 4)], 4, 0); + testCoefficientForSlave("basic self-mating, 1", [new MockMating(1, 1, 3, 4)], 4, 1 / 2); + testCoefficientForSlave("basic self-mating, 2", [new MockMating(1, 1, 3, 4)], 1, 0); + testCoefficientForSlave("basic child-parent mating",[new MockMating(1, 2, 3), new MockMating(2, 3, 4)], 4, 1 / 4); + testCoefficientForSlave("basic sibling mating",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5)], 5, 1 / 4); + testCoefficientForSlave("basic half-sibling mating",[new MockMating(1, 2, 3), new MockMating(2, 4, 5), new MockMating(3, 5, 6)], 6, 1 / 8); + testCoefficientForSlave("basic first cousin mating",[new MockMating(1, 2, 3, 4), new MockMating(5, 3, 6), new MockMating(4, 7, 8), new MockMating(6, 8, 9)], 9, 1 / 16); + testCoefficientForSlave("double first cousin mating",[new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8), + new MockMating(3, 7, 9), new MockMating(4, 8, 10), new MockMating(9, 10, 11)], 11, 1 / 8); + testCoefficientForSlave("aunt-niece mating",[new MockMating(1, 2, 3, 4), new MockMating(4, 5, 6), new MockMating(3, 6, 7)], 7, 1 / 8); + let scenario53 = [new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8), new MockMating(3, 7, 9, 10), new MockMating(4, 8, 11), + new MockMating(12, 9, 13), new MockMating(10, 11, 14), new MockMating(13, 14, 15)]; + testCoefficientForSlave("problem 5.3 from Falconer, 1",scenario53, 14, 1 / 8); // + testCoefficientForSlave("problem 5.3 from Falconer, 2",scenario53, 15, 3 / 32); + testCoefficientForSlave("example in figure 64 from Lancaster",[new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 9), new MockMating(9, 10, 12, 13), + new MockMating(11, 12, 15), new MockMating(13, 14, 16), new MockMating(15, 16, 17, 18)], 17, 33 / 512); + testCoefficientForSlave("example in figure 65 from Lancaster",[new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 10, 11), + new MockMating(9, 10, 13), new MockMating(11, 12, 14), new MockMating(13, 14, 15)], 15, 9 / 128); + testCoefficientForSlave("example in figure 66 from Lancaster",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9)], 9, 1 / 2); + testCoefficientForSlave("example in figure 66 from Lancaster, extended by one more generation's worth of mating between siblings",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11)], 11, 19 / 32); + // from here the preceding scenario of regular sibling-mating is extended by several more steps; by rules given in Falconer, the coefficient ck for step k, where step k follows + // step j which follows step i, should be ck = 1/4 + cj/2 + ci/4, and so that is how the expected values used in the next few tests were calculated + testCoefficientForSlave("extended, 1: 43/64 = 1/4 + (19/32) / 2 + (1/2) / 4",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13)], 13, 43 / 64); + testCoefficientForSlave("extended, 2",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15)], 15, 94 / 128); + testCoefficientForSlave("extended, 3", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17)], 17, 201 / 256); + testCoefficientForSlave("extended, 4", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19)], 19, 423 / 512); + testCoefficientForSlave("extended, 5", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), + new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19, 20), + new MockMating(19, 20, 21)], 21, 880 / 1024); + // Does not apply to current algorithm: as of this writing, the ten-generations-of-sisterfucking test immediately above is enough to occupy the game's coefficient-of-inbreeding calculation algorithm for a few minutes + // on a ~4Ghz CPU. this is very silly. when you read this, that algorithm should have been replaced by a much faster one, allowing the above test to be comfortably run again. + let scenario70 = [new MockMating(1, 2, 6), new MockMating(2, 3, 7), new MockMating(3, 4, 8), new MockMating(5, 6, 9), new MockMating(7, 8, 10), + new MockMating(9, 10, 12), new MockMating(10, 11, 13), new MockMating(12, 13, 14), new MockMating(12, 14, 15)]; // example in figure 70 from Lancaster + testCoefficientForSlave("example in figure 70 from Lancaster, 1", scenario70, 2, 0); + testCoefficientForSlave("example in figure 70 from Lancaster, 2", scenario70, 10, 1 / 8); + testCoefficientForSlave("example in figure 70 from Lancaster, 3", scenario70, 12, 1 / 32); + testCoefficientForSlave("example in figure 70 from Lancaster, 4", scenario70, 15, 85 / 256); + let scenarioXXX = [new MockMating(1, 2, 5, 6), new MockMating(3, 4, 7, 8), new MockMating(5, 6, 9, 10), new MockMating(5, 7, 11), new MockMating(5, 8, 12), + new MockMating(5, 9, 13), new MockMating(9, 10, 14), new MockMating(5, 11, 15), new MockMating(10, 12, 16), new MockMating(13, 16, 17), + new MockMating(4, 16, 18), new MockMating(14, 15, 19), new MockMating(6, 17, 20), new MockMating(6, 18, 21), new MockMating(6, 19, 22), + new MockMating(20, 21, 23), new MockMating(22, 23, 24)]; // an invented example meant to represent a typical situation in a respectable arcology + testCoefficientForSlave("change, 1",scenarioXXX, 24, 633 / 2048); // here the expected coefficient was retrieved from the algorithm being tested and was not separately verified by hand, + // so this test could conceivably be wrong, but at least it can catch changes... + testCoefficientForSlave("change, 2",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), new MockMating(9, 10, 11, 12), + new MockMating(11, 12, 13), new MockMating(13, 14, 15), + new MockMating(5, 5, 16), new MockMating(15, 16, 17)], 17, 1 / 4); // an example in which the old and new algorithms actually gave different results - the first one such to have been observed! It seems that the old algorithm was wrong + testCoefficientForSlave("negative IDs, 1",[new MockMating(-120, -120, 87)], 87, 1 / 2); // negative IDs need to be accepted (not bothering to test any weird special IDs like -1 here though) + testCoefficientForSlave("negative IDs, 2",[new MockMating(-999, -998, -300), new MockMating(-300, -998, 300)], 300, 1 / 4); + + // Do this last + App.Testing.unitDone(); +} diff --git a/tests/index.js b/tests/index.js index d75acce4a90..91d932d6e16 100644 --- a/tests/index.js +++ b/tests/index.js @@ -107,14 +107,27 @@ App.Testing = (function() { } } + function notNaN(value) { + if (Number.isNaN(value)) { + throw new TestError("Expected valid number, got NaN."); + } + } + + function inRange(value, target, tolerance) { + if (Math.abs(value - target) > tolerance) { + throw new TestError(`Value ${value} outside range. Expected ${target} with tolerance ${tolerance}.`); + } + } + return { addTestUnit, start, unitDone, executeTest, - equals, hasProperty, hasNoProperty: hasNoProperty, isType + equals, hasProperty, hasNoProperty: hasNoProperty, isType, notNaN, inRange, }; })(); // Now load all tests App.Testing.addTestUnit("diffProxy"); +App.Testing.addTestUnit("ibc"); // Finally, execute the tests App.Testing.start(); -- GitLab