-- 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) -- fysisk optaget: et køretøj holder på punktet uden reservation (fx efter omplanlægning) -> spring over local occ = ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = waitPoints[1].x + 2, z = waitPoints[1].z}}, 6) check("optaget punkt markeres", occ[waitPoints[1].marker.id], true) check("optaget springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, nil, occ).marker.id, 2) check("egen reservation vinder over optaget (det er en selv)", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1, nil, occ).marker.id, 1) check("køretøj langt væk optager intet", next(ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = 9999, z = 9999}}, 6)), nil) -- ét køretøj mellem to punkter optager kun det NÆRMESTE local between = ADSupplyPlanner.markOccupiedWaitPoints({{marker = {id = 7}, x = 0, z = 0}, {marker = {id = 8}, x = 6, z = 0}}, {{x = 2, z = 0}}, 6) check("kun nærmeste punkt optages", (between[7] == true and between[8] == nil), true) -- 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) -- canUnloadRest med run-tærskel: "under 30 %" = fri >= 70 % af kapaciteten check("tærskel 30 %: 60 % fri er ikke nok", ADSupplyPlanner.canUnloadRest(60000, 90000, 100000, 0.7), false) check("tærskel 30 %: 70 % fri er nok", ADSupplyPlanner.canUnloadRest(70000, 90000, 100000, 0.7), true) check("hele resten passer vinder altid", ADSupplyPlanner.canUnloadRest(5000, 5000, 100000, 0.7), true) check("uden tærskel = 50 %-reglen", ADSupplyPlanner.canUnloadRest(50000, 90000, 100000), true) -- rankNeeds: fælles trug (scoreOverride) + fodervægt (priority): TMR før hø før græs, -- men et separat halmlager der er tommere vinder over truget local TMR, HAYX = 40, 41 local trough = ADSupplyPlanner.rankNeeds({ {fillType = GRASS, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.4}, {fillType = TMR, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 1.0}, {fillType = HAYX, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.8}, {fillType = HAY, level = 20000, capacity = 100000}, -- halm-agtigt separat lager, 80 % tomt }) check("separat lager 80 % tomt vinder over trug 30 % tomt", trough[1].fillType, HAY) check("i truget: TMR (1.0) først", trough[2].fillType, TMR) check("så hø (0.8)", trough[3].fillType, HAYX) check("så græs (0.4)", trough[4].fillType, GRASS) check("scoreOverride bruges som score", trough[2].score, 0.3) -- Bedste foder først: lavere foder (ensilage 0,8) må kun hentes når truget er nødlidende -- (fri ≥ EMERGENCY_TROUGH_FREE af kapaciteten), hvis der er bedre foder (TMR 1,0) i samme trug local tmrNeed = {fillType = 116, priority = 1.0, score = 0.6, trough = true} local silageNeed = {fillType = 7, priority = 0.8, score = 0.6, trough = true} local strawNeed = {fillType = 31, priority = 0, score = 0.9} check("NF1 bedste foder tillades altid", ADSupplyPlanner.isFoodAllowed(tmrNeed, 1.0), true) check("NF1 ensilage afvist ved halvfuldt trug", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), false) silageNeed.score = 0.8 check("NF1 ensilage tilladt når truget er næsten tomt", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), true) check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(strawNeed, 1.0), true) check("NF1 intet bedre foder på listen -> tilladt", ADSupplyPlanner.isFoodAllowed({fillType = 7, priority = 0.8, score = 0.1, trough = true}, 0.8), true) check("NF1 bedste foderprioritet", ADSupplyPlanner.bestFoodPriority({tmrNeed, silageNeed, strawNeed}), 1.0) -- Læs på vej (andre traktorer): lægges oven i målets niveau; foder deler trug -> hele trugets fri plads falder local pendingNeeds = { {fillType = 31, level = 5000, capacity = 20000}, -- halm {fillType = 116, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- TMR, tomt trug {fillType = 7, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- ensilage } local adjusted = ADSupplyPlanner.applyPending(pendingNeeds, {[116] = 20000, [31] = 20000}) check("AP halm: niveau + på vej, højst kapacitet", adjusted[1].level, 20000) check("AP TMR: niveau + på vej", adjusted[2].level, 20000) check("AP TMR: kapacitet = niveau + resterende fri", adjusted[2].capacity, 30000) check("AP ensilage: truget har kun 10000 fri tilbage", adjusted[3].capacity - adjusted[3].level, 10000) check("AP trug-score falder", math.floor(adjusted[3].scoreOverride * 100 + 0.5), 33) check("AP input urørt", pendingNeeds[2].level, 0) check("AP intet på vej -> samme værdier", ADSupplyPlanner.applyPending(pendingNeeds, {})[1].level, 5000) local ranked = ADSupplyPlanner.rankNeeds(adjusted, 8000) check("AP halm fuld (på vej) -> kun trug-varer tilbage", #ranked, 2) -- PF: parallelt foder (høns: korn 80 % / roesnitter 20 % af det de spiser) — hver gruppe har sin andel af truget do local WHEAT, BARLEY, BEETCUT = 1, 2, 3 local groups = {{fillTypes = {WHEAT, BARLEY}, eatWeight = 0.8}, {fillTypes = {BEETCUT}, eatWeight = 0.2}} local levels = {[WHEAT] = 60000, [BARLEY] = 20000, [BEETCUT] = 0} local lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BEETCUT) check("PF roesnitter: andel 20k, har 0", lvl .. "/" .. cap, "0/20000") lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BARLEY) check("PF korn-gruppen samlet: 80k af 80k", lvl .. "/" .. cap, "80000/80000") check("PF ukendt vare -> nil", ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, 99), nil) end -- cargoFor: læs der allerede er på vognen ved start tæller som "på vej" (in-game 27/9: tre fyldte -- biogas-traktorer startet fra fanen kørte alle til Drop 1 — ingen var registreret på vej) check("CF run-vare på vognen", select(2, ADSupplyPlanner.cargoFor(SILAGE, 427215, {SILAGE})), 427215) check("CF vare uden for run'et -> nil", ADSupplyPlanner.cargoFor(HAY, 5000, {SILAGE}), nil) check("CF tom vogn -> nil", ADSupplyPlanner.cargoFor(SILAGE, 0, {SILAGE}), nil) check("CF ukendt vare -> nil", ADSupplyPlanner.cargoFor(nil, 5000, {SILAGE}), nil) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)