-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden) -- Tester de rene behovs-funktioner; ingen spil- eller AutoDrive-mocks nødvendige. dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") local HAY, GRASS, SILAGE = 20, 21, 22 local failures = 0 local function check(name, actual, expected) if actual == expected then print("OK " .. name) else failures = failures + 1 print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end end -- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0 check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true) check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0) -- rankNeeds: mest trængende først, fulde springes over, input muteres ikke local needs = { {fillType = HAY, level = 80000, capacity = 100000}, {fillType = GRASS, level = 20000, capacity = 100000}, {fillType = SILAGE, level = 99500, capacity = 100000}, } local ranked = ADSupplyPlanner.rankNeeds(needs) check("fuld ingrediens springes over", #ranked, 2) check("mest trængende først", ranked[1].fillType, GRASS) check("dernæst næstmest trængende", ranked[2].fillType, HAY) check("freeSpace beregnes", ranked[1].freeSpace, 80000) check("input muteres ikke", needs[1].freeSpace, nil) check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0) -- rankNeeds: deterministisk ved lige score (laveste fillType først) local tie = ADSupplyPlanner.rankNeeds({ {fillType = SILAGE, level = 0, capacity = 5000}, {fillType = HAY, level = 0, capacity = 5000}, }) check("lige score -> laveste fillType først", tie[1].fillType, HAY) -- resolveIngredients local multi = {HAY, GRASS} local resolved = ADSupplyPlanner.resolveIngredients(nil, multi) check("multi-valg bliver ingrediensliste", #resolved, 2) check("listen er en kopi", resolved ~= multi, true) check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil) local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS} check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3) check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil) check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2) check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil) -- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true) check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true) check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false) check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false) check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false) check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true) check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false) check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true) check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true) -- Fabriksstyret forsyning: hvilke produktioner "ønsker" input? local MISSING_INPUTS, INACTIVE = 2, 0 check("tændt produktion ønskes", ADSupplyPlanner.isProductionWanted(true, INACTIVE, MISSING_INPUTS), true) check("auto-slukket pga. mangel ønskes stadig", ADSupplyPlanner.isProductionWanted(false, MISSING_INPUTS, MISSING_INPUTS), true) check("manuelt slukket ønskes ikke", ADSupplyPlanner.isProductionWanted(false, INACTIVE, MISSING_INPUTS), false) check("ukendt status uden tænd -> ikke ønsket", ADSupplyPlanner.isProductionWanted(false, nil, MISSING_INPUTS), false) -- collectProductionDemand: liter/min pr. vare fra ønskede produktioner, kun det vognen kan bære local STRAW, MINERAL, WATER = 23, 24, 25 local productions = { {id = "tmr", cyclesPerMinute = 2, inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}, {type = MINERAL, amount = 1}}}, {id = "mix", cyclesPerMinute = 1, inputs = {{type = SILAGE, amount = 50}, {type = WATER, amount = 500}}}, {id = "off", cyclesPerMinute = 5, inputs = {{type = HAY, amount = 100}}}, } local wanted = {tmr = true, mix = true} local canCarry = {[SILAGE] = true, [STRAW] = true, [MINERAL] = true, [HAY] = true} local demand = ADSupplyPlanner.collectProductionDemand(productions, function(p) return wanted[p.id] == true end, canCarry) check("forbrug summeres på tværs af produktioner", demand[SILAGE], 250) check("forbrug ganges med cyklusser/min", demand[STRAW], 20) check("uønsket produktion tæller ikke", demand[HAY], nil) check("vare vognen ikke kan bære udelades", demand[WATER], nil) check("tom produktionsliste -> tomt behov", next(ADSupplyPlanner.collectProductionDemand({}, function() return true end, canCarry)), nil) check("produktion uden inputs tåles", next(ADSupplyPlanner.collectProductionDemand({{id = "x", cyclesPerMinute = 1}}, function() return true end, canCarry)), nil) -- minutesToEmpty check("minutter til tom", ADSupplyPlanner.minutesToEmpty(1000, 250), 4) check("intet forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, 0), nil) check("ukendt forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, nil), nil) -- rankNeeds med forbrug: den der løber tør først vinder, selv om den er relativt fyldest local timed = ADSupplyPlanner.rankNeeds({ {fillType = MINERAL, level = 500, capacity = 10000, litersPerMinute = 2}, -- 250 min, 95 % tom {fillType = SILAGE, level = 50000, capacity = 100000, litersPerMinute = 250}, -- 200 min, 50 % tom {fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, -- 4500 min }) check("kortest tid til tom først", timed[1].fillType, SILAGE) check("dernæst næstkortest", timed[2].fillType, MINERAL) check("minutesToEmpty følger med", timed[1].minutesToEmpty, 200) check("vare med forbrug slår vare uden", ADSupplyPlanner.rankNeeds({ {fillType = HAY, level = 0, capacity = 100000}, {fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, })[1].fillType, STRAW) check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({ {fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20}, }), 0) -- isProductionInput: en fabriks input må aldrig stjæles som kilde — medmindre -- fabrikken også selv producerer varen (så er den et output, der må hentes) local TMR = 26 local factory = { {id = "tmr", inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}}, outputs = {{type = TMR, amount = 50}}}, {id = "pellets", inputs = {{type = TMR, amount = 10}}, outputs = {{type = HAY, amount = 5}}}, } check("input til fabrikken er beskyttet", ADSupplyPlanner.isProductionInput(factory, STRAW), true) check("output må hentes", ADSupplyPlanner.isProductionInput(factory, HAY), false) check("både input og output (kæde) -> må hentes", ADSupplyPlanner.isProductionInput(factory, TMR), false) check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProductionInput(factory, WATER), false) check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false) check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false) -- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende local sized = ADSupplyPlanner.rankNeeds({ {fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri {fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri }, 40000) check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR) check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW) check("fits markeres", sized[1].fits, true) check("ikke-fits markeres", sized[2].fits, false) local noneFit = ADSupplyPlanner.rankNeeds({ {fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, {fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50}, }, 40000) check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW) check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil) -- Wait-punkter: MAPPEN har ordet "Wait" (uanset store/små); markørnavnet er ligegyldigt. -- (Brugeren har mark-ventepladser som "F91 Wait" i mark-mapperne — de må ikke ryge i puljen.) check("mappen 'Cow Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "Cow Wait", group = "Cow Wait"}), true) check("mappen 'Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true) check("mappen 'wait stalde' tæller", ADSupplyPlanner.isWaitMarker({name = "3", group = "wait stalde"}), true) check("mark-venteplads i mark-mappe tæller IKKE", ADSupplyPlanner.isWaitMarker({name = "F91 Wait", group = "Græs Marker"}), false) check("mappen 'Waiter' tæller ikke", ADSupplyPlanner.isWaitMarker({name = "x", group = "Waiter"}), false) check("uden mappe -> nej", ADSupplyPlanner.isWaitMarker({name = "Wait 1"}), false) -- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle} local V1, V2 = {}, {} local waitPoints = { {marker = {id = 1, name = "Wait A"}, x = 0, z = 0}, {marker = {id = 2, name = "Wait B"}, x = 100, z = 0}, {marker = {id = 3, name = "Wait C"}, x = 300, z = 0}, } check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1) check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2) check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1) check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil) check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil) -- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv) local unreachableA = function(candidate) return candidate.marker.id ~= 1 end check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2) check("intet nåeligt -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, function() return false end), nil) -- Hjem fra Wait: fri plads >= min(rest, halvdelen af målets kapacitet) -- (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald) check("hele resten passer -> hjem", ADSupplyPlanner.canUnloadRest(35000, 35000, 300000), true) check("resten passer ikke endnu -> vent", ADSupplyPlanner.canUnloadRest(5000, 20000, 50000), false) check("kæmpe rest: halv stald fri -> hjem", ADSupplyPlanner.canUnloadRest(150000, 700000, 300000), true) check("kæmpe rest: under halv stald fri -> vent", ADSupplyPlanner.canUnloadRest(100000, 700000, 300000), false) check("ukendt kapacitet -> kun hele resten tæller", ADSupplyPlanner.canUnloadRest(100000, 700000, nil), false) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)