b8da94a254
In-game 2026-09-22 21:37: Fastrac omdirigeret til 'Cow Wait' uden vejnet dertil -> AutoDrive 'cannot reach' + StopAndDisableADTask (AD slukket i stalden). Nu: ADGraphManager:getPathTo + pathFromTo (rene opslag) skal ende i hhv. Wait-punkt og mål, ellers næste punkt; intet nåeligt = som i dag + advarsel. 700.000 l-vogn i 300.000 l-stald kunne aldrig opfylde 'hele resten passer': ADSupplyPlanner.canUnloadRest = fri >= min(rest, 50 % af kapacitet).
177 lines
12 KiB
Lua
177 lines
12 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: 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)
|