AutomatedGoonChemiCompiler/main.js

721 lines
21 KiB
JavaScript

const fs = require("fs");
const util = require("util");
const dice = require("fast-dice-coefficient");
const asm = require("./compiler.js");
const { ingredients } = require("./download");
let reference = JSON.parse(fs.readFileSync("base.json"));
function deepCopy(a) {
return JSON.parse(JSON.stringify(a));
}
function isRef(a) {
return reference.includes(a);
}
function isRefOrUnCrafty(a) {
//if this chem has no need to be made or cannot be made.
if (!isRef(a)) {
return !chems.ChemFind(a).recipe.some((x) => x.yields != 0);
} else {
return true;
}
}
function isCrafty(a) {
if (isRef(a)) {
return true;
}
return chems.ChemFind(a).recipe.some((x) => x.yields != 0);
}
class Recipe {
constructor(name, recipe, ignition) {
this.name = name;
this.recipe = recipe;
this.ignition = ignition;
}
}
//a binder.
class asmBind {
static outOfBoundsErrror(int) {
if (int > 10 || int < 1) {
throw "Please enter a digit between 1 and 10";
}
}
static move(amount, source, target, out) {
asmBind.outOfBoundsErrror(source);
asmBind.outOfBoundsErrror(target);
out.push(`mov ${amount}, ${source}, ${target}\n`);
}
static moveMixToOut(out) {
out.push(`movall 9, 10\n`);
}
static setTemp(temprature, out) {
out.push(`temp 9, ${temprature}\n`);
}
static isolate(amount, order, source, target, out) {
asmBind.outOfBoundsErrror(source);
asmBind.outOfBoundsErrror(target);
out.push(`iso ${amount}, ${order}, ${source}, ${target}\n`);
}
static end(out) {
out.push("compile\n");
}
static print(text, out) {
out.push(`print ${text}`);
}
static compile(a) {
return asm.assembler(a);
}
static pill(out) {
out.push("pill 9\n");
}
}
function getYieldModifier(a) {
try {
return a.yields / a.chemarray.reduce((a, b) => a + b[1], 0);
} catch {
return 0;
}
}
let chems = JSON.parse(fs.readFileSync("out.json"));
util.inspect.defaultOptions.depth = null;
//this proc allows me to have lienencey involving naming.
chems.ChemFind = function (a) {
//this is a very pretty way of accounting for string parsing issues.
if (a in chems) {
return chems[a];
} else {
let leven = [];
for (const item in chems) {
leven.push([dice(a, item), item]);
}
leven.sort((a, b) => b[0] - a[0]);
return chems[leven[0][1]];
}
};
chems.getRecipes = function () {
return Object.values(chems)
.map((x) => x.recipe)
.flat()
.filter((x) => x == undefined || x.chemarray.length != 0);
};
// rough thing, kinda just gonna let it be?
function recursiveScore(a) {
function score(a) {
const current = a.chemarray;
let total = current.length * 3;
let points = 0;
for (const x in current) {
if (isRef(current[x][0])) {
points += 3;
continue;
}
if (chems.ChemFind(current[x][0]).yields != 0) {
points++;
} else {
continue;
}
}
return (points / total) * 100;
}
let base = [0, {}];
function recursive(a) {
let filtered = a.recipe.filter((x) => x.yields != 0);
if (filtered.length == 0) {
base[1][a.name] = a.recipe[0];
return;
}
let best;
if (filtered.length == 1) {
base[0] += score(filtered[0]);
base[1][a.name] = filtered[0];
best = filtered[0];
} else {
let comparision = [];
for (let x in filtered) {
comparision.push([x, score(filtered[x])]);
}
comparision.sort((a, b) => b[1] - a[1]);
best = filtered[comparision[0][0]];
base[0] += score(best);
base[1][a.name] = best;
}
let reagents = best.chemarray.filter((x) => !isRef(x[0]));
for (x in reagents) {
recursive(chems.ChemFind(reagents[x][0]));
}
}
recursive(a);
return base[1];
}
chems["thc"] = chems.ChemFind("Tetrahydrocannabinol");
class crafty {
//prohbably gonna get merged with ingredients, its kinda weird that we have two that do the same thing.
constructor(
temp,
chems,
wanted,
order,
name,
ignition,
yields,
yieldmodifier,
) {
this.temp = temp;
this.chems = chems;
this.wanted = wanted;
this.order = order;
this.name = name;
this.ignition = ignition;
this.total = wanted.reduce((a, b) => a + b[1], 0);
this.yields = yields;
this.yieldmodifier = yieldmodifier;
}
}
class beaker {
//this class is on a pretty strict lockdown because troubleshooting this will be impossible
constructor(chems, max = 100, beakernumber) {
this.chems = chems;
this.max = max;
this.beakernumber = beakernumber;
this.temp = 293.15;
}
sortCrafty() {
this.chems = this.chems.sort((a, b) => isCrafty(b[0]) - isCrafty(a[0]));
}
toPreset() {
return this.chems
.filter((x) => isRef(x[0]))
.map((x) => x.join("="))
.join(";");
}
total() {
return this.chems.reduce((a, b) => a + b[1], 0);
}
//returns the reagent name, amount, and order in the beaker
free() {
return [this.max - this.total(), this.beakernumber];
}
getReagent(a) {
let chems = this.chems;
for (var x in chems) {
if (chems[x][0] == a) {
return [chems[x][0], chems[x][1], Number(x) + 1, this.beakernumber];
}
}
return [a, 0, -1, -1];
}
changeReagent(a, b) {
let found = this.chems.find((x) => x[0] == a);
found[1] += b;
if (found[1] < 0) {
//future caroline, this is because the beaker is less than zero.
//ie subracted too much
throw "A beaker has reached a negative balance, This is a bug in the algorithm. Progress is irrecoverable.";
}
if (found[1] == 0) {
this.chems = this.chems.filter((x) => x[0] != a);
}
if (this.total() > this.max) {
throw "A beaker has ovweflowed, This is a bug in the algorithm. Progress is irrecoverable.";
}
}
addReagent(a) {
if (typeof a != typeof []) {
throw "The reagent is the incorrect type. This is a bug in the algorithm. Progress is irrecoverable.";
}
if (a.length != 2) {
throw "The reagent is an array, but isn't in the proper format.";
}
if (this.total >= this.max) {
throw "The beaker cannot fit anymore reagent in, and thus cannot be added to. This is a bug in the algorithm. Progress is irrecoverable. ";
}
if (a[1] == 0) {
return;
}
if (this.chems.some((x) => x[0] == a[0])) {
//adds to the production
this.chems.find((x) => x[0] == a[0])[1] += a[1];
} else {
//other wise it just adds it to the total production quota
this.chems.push(a);
}
if (this.total() > this.max) {
throw "My nam is (u dont need to remember) Guanglai Kangyi, age 15, the past month to me is, only graduation tests. the entrance into high-school brings really exciting and worry-ing days. till i met these 2 very special guys......";
}
}
//added chem is optional we get the existing reactions vs the ones if we add it
premtiveReactions(addeChem = undefined) {
let reactions = chems.getRecipes().filter((x) => x != undefined);
let localchems = this.chems.map((x) => x[0]);
if (addeChem != undefined) {
localchems.push(addeChem);
}
function allIn(addeChem, chems) {
//gets a list of all names of chems
addeChem = addeChem.map((x) => x[0]);
return !addeChem
.map((x) => localchems.includes(x))
.some((x) => x == false);
}
reactions = reactions.filter((x) => allIn(x.chemarray));
return reactions;
}
ifaddedWillReact(a) {
//JS lacks the ability to compare arrays so this is just a makeshift way of doing it.
//we compare existing reactions vs reactions if the chem was added.
let newreaction = this.premtiveReactions(a[0]);
let oldreaction = this.premtiveReactions().map((x) => x.name);
if (oldreaction.join("") != newreaction.map((x) => x.name).join("")) {
return [
true,
newreaction.filter((x) => !oldreaction.includes(x.name)),
this.beakernumber,
this.free(),
];
} else {
return [false, [], this.beakernumber, this.free()];
}
}
}
class BeakerGroup {
constructor(max, recipes) {
this.outBeakers = [new beaker([], 100, 9), new beaker([], max, 10)];
this.beakers = [];
this.recipes = recipes;
}
startCraft() {
let out = [];
this.recipes.forEach((x) => {
out.push(this.craftChem(x));
});
return out.flat();
}
numberOfBeakers() {
return this.beakers.length;
}
totalFree() {
return this.beakers.map((x) => x.free());
}
supplyReagents(chem, destination, outCommands) {
if (chem[1] == 0) {
return;
}
//we get the chems which match the input and sort by largest. We filter it
let avaliable = this.beakers
.map((x) => x.getReagent(chem[0]))
.sort((a, b) => a[1] + b[1])
.filter((x) => x[1] > 0);
let totalfree = 0;
if (avaliable.length > 1) {
totalfree = avaliable.reduce((a, b) => a + b[1], 0);
} else {
totalfree = avaliable[0][1];
}
//this gets the total amount of chems in the beakergroup
if (chem[1] > totalfree) {
console.log(chem);
console.log(totalfree);
console.log(this.beakers);
throw "NOT ENOUGH CHEMS TO SUPPLY!!!!!!!!";
}
//gets the total amount supplied thus far
let totalmoved = 0;
for (x in avaliable) {
//we can break once we are done
if (totalmoved == chem[1]) {
break;
}
let current = avaliable[x];
//due is the amount left we have to transfer
let due = chem[1] - totalmoved;
//this is the difference between what we have to take and which is avaliable
let difference = due - current[1];
//if due isn't zero or less then we have to conform to the difference and draw what we can in future loops
if (difference > 0) {
due -= difference;
}
totalmoved += due;
this.beakers[current[3] - 1].changeReagent(chem[0], 0 - due);
outCommands.push(`#Isolating ${due}u of ${chem[0]} to ${destination}\n`);
asmBind.isolate(due, current[2], current[3], destination, outCommands);
}
}
craftChem(crafty) {
//console.log(crafty)
// console.log(this.beakers)
let outCommands = [];
let mixtemp = this.outBeakers[0].temp;
if (this.ignition >= mixtemp) {
asmBind.setTemp(crafty.ignition - 20, outCommands, mixtemp);
this.outBeakers[0].temp = crafty.ignition - 20;
}
if (crafty.total <= 101) {
for (var x in crafty.wanted) {
this.supplyReagents(crafty.wanted[x], 9, outCommands);
}
if (crafty.temp != -1) {
asmBind.setTemp(crafty.temp + 20, outCommands, mixtemp);
this.outBeakers[0].temp = crafty.temp + 20;
}
if (crafty.order == 0) {
asmBind.moveMixToOut(outCommands);
asmBind.end(outCommands);
} else {
outCommands.push(
`#returning ${crafty.chems}u of ${crafty.name} from mix.\n`,
);
this.returnFromMix([crafty.name, crafty.chems], outCommands);
}
} else {
let due = crafty.total;
let modifier = 100 / due;
let ref = deepCopy(crafty.wanted);
while (true) {
let moved = 0;
let toMove = 0;
let end = false;
if (100 > ref.reduce((a, b) => a + b[1], 0)) {
end = true;
}
for (x in crafty.wanted) {
let wanted = crafty.wanted[x];
if (!end) {
toMove = Math.floor(wanted[1] * modifier);
} else {
toMove = ref[x][1];
}
moved += toMove;
ref[x][1] -= toMove;
due -= toMove;
this.supplyReagents([wanted[0], toMove], 9, outCommands);
}
if (crafty.temp != -1) {
asmBind.setTemp(crafty.temp + 20, outCommands);
this.outBeakers[0].temp = crafty.temp + 20;
}
if (crafty.order != 0) {
outCommands.push(
`#returning ${moved * crafty.yieldmodifier}u of ${crafty.name} from mix.\n`,
);
this.returnFromMix(
[crafty.name, moved * crafty.yieldmodifier],
outCommands,
);
} else if (due != 0) {
asmBind.pill(outCommands);
}
if (end) {
// console.log(due)
break;
}
}
}
//console.log(outCommands.join(""))
return outCommands;
}
returnFromMix(a, outCommands) {
this.avoidReact(a).forEach((x) => {
outCommands.push(`#moving ${x[1]}u of ${a[0]} to ${x[0]}\n`);
asmBind.move(x[1], 9, x[0], outCommands);
});
}
addBeaker(beaker) {
if (8 > this.beakers.length) {
this.beakers.push(beaker);
} else {
throw "You can only fit 8 beakers in a chemi compiler unless you have some whacky stuff invovling moving outs.";
}
}
allocate(free, a) {
//this finds a free location to put a chem
//free is the places it can choose to put it.
let out = [];
let due = a[1];
let sent = 0;
for (x in free) {
let current = free[x];
if (due == 0) {
break;
}
if (current[0] == 0) {
continue;
}
if (current[0] > due) {
this.beakers[current[1] - 1].addReagent([a[0], due]);
sent = due;
due = 0;
} else {
due -= current[0];
this.beakers[current[1] - 1].addReagent([a[0], current[0]]);
sent = current[0];
}
//this logs the chem and the stuff moved
out.push([current[1], sent]);
}
//it is logged so we can issue commands to the compiler to execute these orders.
return out;
}
///This function yields a place where a chemical can be added and it wont react, if a place exists for itl
avoidReact(a) {
let free = this.totalFree();
if (a[1] > free.reduce((a, b) => a[0] + b[0])) {
throw "not enough space to move reagents.";
}
let ReactMap = this.beakers.map((x) => x.ifaddedWillReact(a));
if (
!ReactMap.some(
(x) =>
x[0] == true && ReactMap.map((x) => x[1].temp == -1).includes(true),
)
) {
return this.allocate(free, a);
} else {
ReactMap = ReactMap.filter((x) => !x[0]);
//the throw here is kinda pointless because this function will fail because throw happens
if (a[1] > ReactMap.reduce((a, b) => a + b[3], 0)) {
console.log(ReactMap.reduce((a, b) => a + b[3], 0));
console.log(a);
throw "not enough space to move reagents.";
}
//it requires a map of free(), which is x[3]
return this.allocate(
ReactMap.map((x) => x[3]),
a,
);
}
}
}
function proportionFinder(a, wanted, array, order, OptimalRecipes) {
console.log(a);
//converts the Stoichenemtry proportions to absolute values for production
let StoichiometricValues = [];
//for each element in the chem array
a.chemarray.forEach((current) => {
//we deep copy so the modifications dont alter the entry, for other recipes dependant upon it
current = deepCopy(current);
//this formula is calculates the amount of chems needed.
current[1] = current[1] * Math.ceil(wanted / a.yields);
console.log(current);
console.log(OptimalRecipes);
if (!isRefOrUnCrafty(current[0])) {
proportionFinder(
OptimalRecipes[current[0]],
current[1],
array,
order + 1,
OptimalRecipes,
);
}
StoichiometricValues.push(current);
});
//finds the ignition temprature of chems inside the beakers
//this is to help prevent explosions
if (StoichiometricValues.length != 0) {
let ignitionSearch = StoichiometricValues.map(
(x) => chems.ChemFind(x[0]).ignition,
).filter((x) => x != 0);
let ignition = a.ignition;
//if there is any of them which are sensitive then the ignition temp is set to that.
if (!ignitionSearch.length == 0) {
ignition = ignitionSearch.sort((a, b) => b - a)[0];
}
let ym = getYieldModifier(a);
//the total number of parts times the yield modifier is equal to the total output
//this is done because the rounding makes things funky
let newtotal = StoichiometricValues.reduce((a, b) => a + b[1], 0) * ym;
if (array.map((x) => x.name).includes(a.name)) {
let found = array.find((x) => x.name == a.name);
for (x in StoichiometricValues) {
found.wanted[x][1] += StoichiometricValues[x][1];
}
found.chems += newtotal;
found.total += StoichiometricValues.reduce((a, b) => a + b[1], 0);
return;
}
array.push(
new crafty(
a.temp,
newtotal,
StoichiometricValues,
order,
a.name,
ignition,
a.yields,
ym,
),
);
}
}
function getInput(a) {
//this gets a detailed list of the raw "materials" used to make the chems.
//takes direct input from ProportionFinder
//this gets all the recipes names and all their components
let genned = a.map((x) => x.name);
let search = a.map((x) => x.wanted);
let needed = {};
for (x in search) {
//if a chem has a recipe we remove it because we're making it, it is not needed to be in the raw input
search[x] = search[x].filter((x) => !genned.includes(x[0]));
}
search = search.flat();
search.forEach((x) => {
//we create a table of the every instance of the recipes
if (needed[x[0]] !== undefined) {
//if it exists we add to it
needed[x[0]] += x[1];
} else {
//if it doesn't, we start it
needed[x[0]] = x[1];
}
});
//tbh I'd prefer a pair situation rather than a object {} for various reasons
let keys = Object.keys(needed);
let out = [];
for (var x in keys) {
out.push([keys[x], Math.ceil(needed[keys[x]])]);
}
let total = out.reduce((a, b) => a + b[1], 0);
//beaker groups are the language of allocation
//allowing all beakers to be treated as one distrbuted object is an adaptable framework.
let beakergroup = new BeakerGroup(100, a);
beakersneeded = Math.ceil(total / 100);
//1 beaker is bad because we cant avoid reactions.
if (beakersneeded == 1) {
beakersneeded++;
}
for (i = 1; i != beakersneeded + 1; i++) {
beakergroup.addBeaker(new beaker([], 100, i));
}
//this is the way we assign chems to beakers as to avoid all reactions where possible
out.forEach((x) => {
beakergroup.avoidReact(x);
});
//so I think the most effective way of handling side affects is seperate from allocation
//the code will get really nonsensicale
return beakergroup;
}
util.inspect.defaultOptions.depth = null;
function arbitraryToParts(abritrary) {
//the system is programmed to work in parts
//this converts arbitrary input to totals
let partTotal = abritrary.reduce((a, b) => a + b[1], 0);
abritrary = abritrary.map((x) => [x[0], x[1] / partTotal]);
let input = new Recipe(
"output",
[
new ingredients(
"N/A",
-1,
parts.reduce((a, b) => a + b[1], 0),
parts,
"out",
0,
),
],
0,
);
return [input, partTotal];
}
function main(input, debug = false) {
let wanted = 0;
if (input.length == 1) {
wanted = input[0][1];
input = chems.ChemFind(input[0][0]);
} else {
let converted = arbitraryToParts(input);
input = converted[0];
wanted = converted[1];
}
let out = [];
let OptimalRecipes = recursiveScore(input);
let base = Object.values(OptimalRecipes)[0];
proportionFinder(base, wanted, out, 0, OptimalRecipes);
let beakers = getInput(out.sort((a, b) => b.order - a.order));
beakers.beakers.forEach((x) => {
x.sortCrafty();
console.log(x.toPreset());
console.log(x);
});
let outcommands = beakers.startCraft();
let nondebug = outcommands.filter((x) => x[0] != "#");
let output = asmBind.compile(nondebug.join(""));
if (!debug) {
console.log(nondebug.join(""));
console.log(output);
} else {
console.log(outcommands.join(""));
}
let mood = "\x1b[36m%s\x1b[0m";
if (output.length > 2000) {
mood = "\x1b[31m%s\x1b[0m";
}
console.log(
"\x1b[36m%s\x1b[0m",
`number of lines of code = ${nondebug.length}`,
);
console.log(
"\x1b[36m%s\x1b[0m",
`total u output should be ~${input.recipe[0].yields}`,
);
console.log(mood, `total amount of chars of code are ${output.length}`);
if (mood.includes("31")) {
console.log(
"for some reason the eldritch technology (magic) of the chemicompiler really only supports 2000 chars of code",
);
console.log(
"you must manually split up the commands into bite size pieces in order for the script to not hault midway through.",
);
console.log(
"this might be due to something involving it being 2 kilobyte of data but thats either here nor there.",
);
}
console.log(`expected ouput in exit beaker if lower than 100 chems and in pills if higher.\n
${parts.map((x) => x.join(" ")).join(", ")}`);
console.log(
"not all pills are created equal. Depending on the yield multiplier some may have more some may have less",
);
console.log("meths is 0.75; the maximum output is 75 per pill.");
}
let parts = [["Meth", 100]];
main(parts, false);