diff --git a/FS25_ADSmartPickup/adFieldSiloPlanner.lua b/FS25_ADSmartPickup/adFieldSiloPlanner.lua new file mode 100644 index 0000000..67c950e --- /dev/null +++ b/FS25_ADSmartPickup/adFieldSiloPlanner.lua @@ -0,0 +1,114 @@ +-- AD Field Silo Planner +-- Ren planlægning for marksilo-flowet: hvilke (silo, vare) skal hjem, hvor mange lånte vogne sendes, og til +-- hvilket gårdlager. Ingen spil-kald; input muteres ikke. + +ADFieldSiloPlanner = {} + +function ADFieldSiloPlanner.key(siloId, fillType) + return tostring(siloId) .. ":" .. tostring(fillType) +end +local key = ADFieldSiloPlanner.key + +local function dist2(a, b) + return ((a.x or 0) - (b.x or 0)) ^ 2 + ((a.z or 0) - (b.z or 0)) ^ 2 +end + +local function freeIn(store, fillType, reserved) + return ((store.free or {})[fillType] or 0) - (reserved[store.id .. ":" .. tostring(fillType)] or 0) +end + +local function anyStoreTakes(stores, fillType, minLiters, reserved) + for _, store in ipairs(stores or {}) do + if store.markerId ~= nil and freeIn(store, fillType, reserved) >= minLiters then return true end + end + return false +end + +-- Nærmeste lager med plads til hele turen; ellers det med mest plads (over tærsklen). +local function chooseStore(stores, silo, fillType, liters, minLiters, reserved) + local best, bestD, most, mostFree = nil, nil, nil, nil + for _, store in ipairs(stores or {}) do + local free = freeIn(store, fillType, reserved) + if store.markerId ~= nil and free >= minLiters then + local d = dist2(store, silo) + if free >= liters and (bestD == nil or d < bestD) then best, bestD = store, d end + if mostFree == nil or free > mostFree then most, mostFree = store, free end + end + end + return best or most +end + +local function canCarry(vehicle, fillType) + if (vehicle.capacity or 0) <= 0 then return false end + if vehicle.cargo ~= nil and vehicle.cargo ~= fillType then return false end + return (vehicle.carries or {})[fillType] == true +end + +local function nearestVehicle(vehicles, used, silo, fillType) + local best, bestD = nil, nil + for _, vehicle in ipairs(vehicles or {}) do + if not used[vehicle.id] and canCarry(vehicle, fillType) then + local d = dist2(vehicle, silo) + if bestD == nil or d < bestD then best, bestD = vehicle, d end + end + end + return best +end + +function ADFieldSiloPlanner.plan(input) + local minLiters = input.minLiters or 10000 + local reserved, onWay = {}, {} + for _, loan in ipairs(input.loans or {}) do + local k = key(loan.siloId, loan.fillType) + onWay[k] = (onWay[k] or 0) + (loan.liters or 0) + end + local status, tasks = {}, {} + for _, silo in ipairs(input.silos or {}) do + for fillType, liters in pairs(silo.stock or {}) do + if (liters or 0) >= minLiters then + local k = key(silo.id, fillType) + local entry = {siloId = silo.id, siloName = silo.name, fillType = fillType, liters = liters, onWay = onWay[k] or 0} + if silo.markerId == nil then + entry.state = "noMarker" + elseif not anyStoreTakes(input.stores, fillType, minLiters, reserved) then + entry.state = "noFarmStore" + elseif (input.paused or {})[k] then + entry.state = "paused" + else + entry.state = "ok" + local rest = liters - entry.onWay + if rest >= minLiters then table.insert(tasks, {silo = silo, fillType = fillType, rest = rest}) end + end + table.insert(status, entry) + end + end + end + table.sort(tasks, function(a, b) + if a.rest ~= b.rest then return a.rest > b.rest end + if tostring(a.silo.id) ~= tostring(b.silo.id) then return tostring(a.silo.id) < tostring(b.silo.id) end + return a.fillType < b.fillType + end) + table.sort(status, function(a, b) + if tostring(a.siloId) ~= tostring(b.siloId) then return tostring(a.siloId) < tostring(b.siloId) end + return a.fillType < b.fillType + end) + local freeSlots = math.max(0, (input.maxVehicles or 2) - #(input.loans or {})) + local used, dispatch = {}, {} + for _, task in ipairs(tasks) do + while freeSlots > 0 and task.rest >= minLiters do + local vehicle = nearestVehicle(input.vehicles, used, task.silo, task.fillType) + if vehicle == nil then break end + local liters = math.min(vehicle.capacity, task.rest) + local store = chooseStore(input.stores, task.silo, task.fillType, liters, minLiters, reserved) + if store == nil then break end + used[vehicle.id] = true + local reserveKey = store.id .. ":" .. tostring(task.fillType) + reserved[reserveKey] = (reserved[reserveKey] or 0) + liters + table.insert(dispatch, {vehicleId = vehicle.id, siloId = task.silo.id, siloMarkerId = task.silo.markerId, + fillType = task.fillType, storeId = store.id, storeMarkerId = store.markerId, liters = liters}) + task.rest = task.rest - liters + freeSlots = freeSlots - 1 + end + end + return {dispatch = dispatch, status = status, freeSlots = freeSlots} +end diff --git a/tests/test_adFieldSiloPlanner.lua b/tests/test_adFieldSiloPlanner.lua new file mode 100644 index 0000000..5122809 --- /dev/null +++ b/tests/test_adFieldSiloPlanner.lua @@ -0,0 +1,79 @@ +-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua") +local P = ADFieldSiloPlanner +local WHEAT, CANOLA = 1, 2 +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 +local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end +local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end +local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end +local function base(over) + local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000} + for k, v in pairs(over or {}) do input[k] = v end + return input +end + +-- kvalificering +local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}})) +check("under tærskel -> ingen afsendelse", #r.dispatch, 0) +check("under tærskel -> ingen status", #r.status, 0) +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}})) +check("uden markør -> noMarker", r.status[1].state, "noMarker") +check("uden markør -> ingen afsendelse", #r.dispatch, 0) +r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}})) +check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore") +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}})) +check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0") + +-- afsendelse, fuldeste først, x-loft +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}})) +check("x=2 -> to afsendt", #r.dispatch, 2) +check("fuldeste silo først", r.dispatch[1].siloId, "B") +check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2") +check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B") +check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm") +check("liter = vognens kapacitet", r.dispatch[1].liters, 50000) +check("anden tur = resten", r.dispatch[2].liters, 30000) + +-- aktive lån tæller mod x og mod siloens rest +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}})) +check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1) +check("status viser på vej", r.status[1].onWay, 50000) +r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}})) +check("x=1 med ét lån ude -> ingen", #r.dispatch, 0) +check("frie pladser 0", r.freeSlots, 0) + +-- flere varer i én silo = hver sin opgave +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}})) +check("to varer -> to afsendelser", #r.dispatch, 2) +check("fuldeste vare først", r.dispatch[1].fillType, WHEAT) + +-- vogne der ikke kan +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}})) +check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2") +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}})) +check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2") +r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}})) +check("vogn uden kapacitet -> ingen", #r.dispatch, 0) + +-- lagervalg +r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}})) +check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far") +r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}})) +check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small") +check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny") +r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})) +check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore") + +-- input muteres ikke +local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}) +P.plan(input) +check("input.stores urørt", input.stores[1].free[WHEAT], 1000000) + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)