Files
fs25-adsmartpickup/tests/test_adSupplyPlanner.lua
T
masterdraco 39fccefa40 feat: Wait-punkter for rest-læs + læs der passer foretrækkes (v1.7.0.0)
Rest-læs ved fuldt mål: AutoDrive ventede ved aflæsningspunktet 'til tømt' og
spærrede køen. Nu omdirigeres den aktuelle UnloadAtDestinationTask (destinationID +
drivePathModule:setPathTo) til nærmeste ledige markør med 'Wait' i navnet / mappen
Wait (fælles pulje, reservationer pr. køretøj); tilbage når målet har plads til
hele resten. Ventende ser rotateTargets=RT_NONE så 'rotér aflæsning' ikke kører
videre fra Wait-punktet. Ingen ledige punkter = som i dag + én advarsel.

Rangering: varer hvor et fuldt læs (AutoDrive.getAllFillLevels-kapacitet) passer i
målet går forud; passer intet, mest trængende. Nyt modul adUnloadWait.lua;
ADSmartPickup.getTargetFreeCapacity delt hjælper.
2026-09-22 21:12:55 +02:00

163 lines
10 KiB
Lua

-- 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: navn med ordet "Wait" (uanset store/små) eller mappen "Wait"
check("'Cow 1 Food Wait' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food Wait", group = "Cow Food"}), true)
check("'wait 3' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "wait 3", group = "Andet"}), true)
check("mappen Wait tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true)
check("'Waiter' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Waiter", group = "X"}), false)
check("'Cow 1 Food' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food", group = "Cow Food"}), 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)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)