From 9cab4b937d37c2d28a4bfffc7406bcaaa0f4dfe2 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Fri, 25 Sep 2026 23:51:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(marker):=20model=20for=20h=C3=B8st,=20klar?= =?UTF-8?q?g=C3=B8ring,=20pleje=20og=20multi-tool=20(1-5=20s=C3=A6t,=20afl?= =?UTF-8?q?=C3=A6sningsvogne)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adFieldModel.lua | 308 +++++++++++++++++++++++----- tests/test_adFieldModel.lua | 95 ++++++++- 2 files changed, 342 insertions(+), 61 deletions(-) diff --git a/FS25_ADSmartPickup/adFieldModel.lua b/FS25_ADSmartPickup/adFieldModel.lua index 0a0c274..d85a30a 100644 --- a/FS25_ADSmartPickup/adFieldModel.lua +++ b/FS25_ADSmartPickup/adFieldModel.lua @@ -1,7 +1,14 @@ -- AD Field Model -- Markarbejdets regler som ren data (ingen spil-API): en marks plan og trin, hvornår den er klar, --- hvilket trin der følger, hvor mange sæt et trin må have, og bredde-reglerne. Alle funktioner --- returnerer nye tabeller; job-løkken (adFieldJobs) og GUI'et kalder kun herind. +-- hvilket trin der følger, hvor mange sæt et trin må have, pleje-behov og bredde-reglerne. Alle +-- funktioner returnerer nye tabeller; job-løkken (adFieldJobs/adFieldWork) og GUI'et kalder kun herind. +-- +-- Markens art (kind): "mowable" (græs/kløver/lucerne), "harvest" (alt andet der høstes), "bare" (ingen +-- afgrøde: stub, pløjet, harvet). Kæder: +-- slåafgrøde: mowing -> [tedding] -> [raking] -> collecting +-- høst: harvesting -> klargøring +-- klargøring: [mulching] -> [liming] -> [plowing|cultivating] -> [sowing] -> [rolling] +-- Pleje under vækst (enkelt trin, derefter tilbage til idle): fertilizing, weeding, liming. ADFieldModel = {} @@ -10,11 +17,35 @@ ADFieldModel.CUT_RATIO = 0.5 ADFieldModel.CP_MAX_WIDTH = 50 ADFieldModel.WIDTH_TOLERANCE = 0.05 ADFieldModel.RETRY_MS = 300000 -ADFieldModel.MAX_COLLECTORS = 2 ADFieldModel.CP_MAX_TOOL_OFFSET = 10 +ADFieldModel.MAX_RIGS = 5 +ADFieldModel.MAX_UNLOADERS = 3 +ADFieldModel.WEED_MIN = 2 -- weedState 1 = spredt, 2+ = tæt (AI-Bonde/Arbejdsseddel) +ADFieldModel.WEED_MECHANICAL_MAX = 4 -- større ukrudt kræver sprøjte, strigle kan ikke +ADFieldModel.MAX_CARE_RUNS = 2 -- samme pleje højst to gange pr. vækstcyklus (undgår løkker) -ADFieldModel.ROLES = {mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector"} -local KNOWN_STAGES = {idle = true, ready = true, mowing = true, tedding = true, raking = true, collecting = true, done = true} +ADFieldModel.ROLES = { + mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector", + harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow", + cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder", + fertilizing = "fertilizer", +} +ADFieldModel.HELPERS = {harvesting = "unloader"} +ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true} +-- trin med afgrøde der skal være væk bagefter (ellers sendes maskinen ud igen) +ADFieldModel.CROP_STAGES = {mowing = true, harvesting = true} + +local KNOWN_STAGES = {idle = true, ready = true, done = true} +for stage in pairs(ADFieldModel.ROLES) do KNOWN_STAGES[stage] = true end +KNOWN_STAGES.careLiming = true + +local DEFAULT_PLAN = { + mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, + mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false, + fertilize = true, weed = true, +} +local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true} +local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true} local function copyList(list) local copy = {} @@ -22,35 +53,80 @@ local function copyList(list) return copy end +local function copyPlan(plan) + local copy = {} + for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end + for key, value in pairs(plan or {}) do copy[key] = value end + copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil) + return copy +end + local function copyField(field) local copy = {} for key, value in pairs(field) do copy[key] = value end - copy.plan = { - mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake, - storeBuildingIds = copyList(field.plan.storeBuildingIds), - } + copy.plan = copyPlan(field.plan) + local care = {} + for key, value in pairs(field.careRuns or {}) do care[key] = value end + copy.careRuns = care return copy end +local function clamp(value, low, high) + return math.max(low, math.min(high, math.floor(tonumber(value) or low))) +end + function ADFieldModel.newField(id) return { id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false, - plan = {mode = "manual", product = "silage", rake = false, storeBuildingIds = {}}, + careRuns = {}, plan = copyPlan(nil), } end --- Hø vendes; Riv kan vælges til både hø og ensilage (samler skårene midt i rivens baner, så også --- smalle standard-læssevogne kan tage dem). -function ADFieldModel.workStages(plan) +-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst. +function ADFieldModel.product(plan, kind) + if plan.product ~= nil and plan.product ~= "auto" then return plan.product end + return kind == "mowable" and "silage" or "harvest" +end + +-- Klargøringskæden efter høst (eller på en bar mark). needs = {lime = bool, plow = bool} fra marken. +function ADFieldModel.prepStages(plan, needs) + needs = needs or {} + local stages = {} + if plan.mulch then table.insert(stages, "mulching") end + if plan.lime and needs.lime then table.insert(stages, "liming") end + local soil = plan.soil or "auto" + if soil == "plow" or (soil == "auto" and needs.plow) then + table.insert(stages, "plowing") + elseif soil == "cultivate" or soil == "auto" then + table.insert(stages, "cultivating") + end + if plan.sow then + table.insert(stages, "sowing") + if plan.roll then table.insert(stages, "rolling") end + end + return stages +end + +-- Hele kæden for markens art. kind nil = slåafgrøde (bagudkompatibelt med etape 1). +function ADFieldModel.workStages(plan, kind, needs) + kind = kind or "mowable" + if kind == "bare" then + return ADFieldModel.prepStages(plan, needs) + end + local product = ADFieldModel.product(plan, kind) + if product == "harvest" then + local stages = {"harvesting"} + for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end + return stages + end local stages = {"mowing"} - if plan.product == "hay" then table.insert(stages, "tedding") end + if product == "hay" then table.insert(stages, "tedding") end if plan.rake then table.insert(stages, "raking") end table.insert(stages, "collecting") return stages end --- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner. Vender og river kører --- hele marken i deres egen bredde; efter riv ligger skåret midt i banen, så vognens bredde er ligegyldig. +-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner. function ADFieldModel.requiredWidth(field, role) if role == "collector" and not field.plan.rake then return field.mowWidth @@ -58,27 +134,46 @@ function ADFieldModel.requiredWidth(field, role) return nil end --- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene (rivens kursus, ellers --- slåmaskinens); de andre laver deres eget i deres egen bredde. +-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget. function ADFieldModel.courseSource(field, role) if role ~= "collector" then return nil end return field.plan.rake and "rake" or "mower" end function ADFieldModel.roleFor(stage) + if stage == "careLiming" then return "limer" end return ADFieldModel.ROLES[stage] end +function ADFieldModel.helperFor(stage) + return ADFieldModel.HELPERS[stage] +end + function ADFieldModel.isWorking(field) - return ADFieldModel.ROLES[field.stage] ~= nil + return ADFieldModel.roleFor(field.stage) ~= nil end -function ADFieldModel.maxRigs(stage) - return stage == "collecting" and ADFieldModel.MAX_COLLECTORS or 1 +function ADFieldModel.isCare(stage) + return ADFieldModel.CARE_STAGES[stage] == true end --- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis marken blev slået uden os. --- Arbejdende marker styres af job-løkken, ikke af vækst. +-- Antal sæt pr. trin (multi-tool 1–5) og aflæsningsvogne under høst (1–3). +function ADFieldModel.maxRigs(stage, plan) + return clamp(plan ~= nil and plan.rigs or 1, 1, ADFieldModel.MAX_RIGS) +end + +function ADFieldModel.maxHelpers(stage, plan) + if ADFieldModel.helperFor(stage) == nil then return 0 end + return clamp(plan ~= nil and plan.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS) +end + +-- Courseplays multi-tool-baner (laneOffset) for n sæt: 2 -> -1,1 · 3 -> -1,0,1 · 4 -> -2,-1,1,2 · 5 -> -2..2 +function ADFieldModel.lanePositions(n) + local lanes = {[1] = {0}, [2] = {-1, 1}, [3] = {-1, 0, 1}, [4] = {-2, -1, 1, 2}, [5] = {-2, -1, 0, 1, 2}} + return copyList(lanes[clamp(n, 1, 5)]) +end + +-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os. function ADFieldModel.onReadiness(field, ratio) local copy = copyField(field) if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then @@ -102,9 +197,7 @@ function ADFieldModel.forceReady(field) return field end -function ADFieldModel.start(field) - local copy = copyField(field) - copy.stage = ADFieldModel.workStages(field.plan)[1] +local function freshCycle(copy) copy.startRequested = false copy.retryAt = 0 copy.reason = nil @@ -112,51 +205,120 @@ function ADFieldModel.start(field) copy.mowVehicleId = nil copy.rakeVehicleId = nil copy.windrowOffset = nil + copy.careRuns = {} +end + +-- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig. +function ADFieldModel.start(field, kind, needs) + local copy = copyField(field) + local stages = ADFieldModel.workStages(field.plan, kind, needs) + copy.stage = stages[1] or "done" + copy.kind = kind or "mowable" + freshCycle(copy) return copy end -function ADFieldModel.advance(field) +-- Næste trin i kæden (behov genberegnes, så fx kalk springes over hvis jorden allerede er kalket). +-- Pleje-trin går tilbage til idle. +function ADFieldModel.advance(field, needs) local copy = copyField(field) - local stages = ADFieldModel.workStages(field.plan) - copy.stage = "done" - for index, stage in ipairs(stages) do - if stage == field.stage and stages[index + 1] ~= nil then - copy.stage = stages[index + 1] - end - end copy.retryAt = 0 copy.reason = nil + if ADFieldModel.isCare(field.stage) then + copy.stage = "idle" + return copy + end + local stages = ADFieldModel.workStages(field.plan, field.kind, needs) + copy.stage = "done" + local passed = false + local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"} + local rank = {} + for index, stage in ipairs(order) do rank[stage] = index end + for _, stage in ipairs(stages) do + if not passed and (rank[stage] or 0) > (rank[field.stage] or 0) then + copy.stage = stage + passed = true + end + end return copy end --- Stop fra menuen: arbejdet afbrydes; marken står klar igen (manuel start kræves). +-- Stop fra menuen: arbejdet afbrydes; marken står klar (hovedkæde) eller venter (pleje/klargøring). function ADFieldModel.stop(field) local copy = copyField(field) - if ADFieldModel.isWorking(field) then + if ADFieldModel.CROP_STAGES[field.stage] or field.stage == "tedding" or field.stage == "raking" or field.stage == "collecting" then copy.stage = "ready" + elseif ADFieldModel.isWorking(field) then + copy.stage = "idle" end copy.startRequested = false copy.reason = nil return copy end --- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst slåbredden. -function ADFieldModel.checkWidth(role, width, mowWidth) +-- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields: +-- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus. +function ADFieldModel.careNeed(field, info) + if not (field.stage == "idle" or field.stage == "done") or info == nil or not info.growing then + return nil + end + local runs = field.careRuns or {} + local function allowed(stage) return (runs[stage] or 0) < ADFieldModel.MAX_CARE_RUNS end + if field.plan.weed and info.kind == "harvest" and (info.weed or 0) >= ADFieldModel.WEED_MIN and allowed("weeding") then + return "weeding" + end + if field.plan.fertilize and info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax and allowed("fertilizing") then + return "fertilizing" + end + if field.plan.lime and info.lime ~= nil and info.lime < 1 and allowed("careLiming") then + return "careLiming" + end + return nil +end + +-- Start et pleje-trin (tæller kørslen, så samme pleje ikke gentages i det uendelige). +function ADFieldModel.startCare(field, stage) + local copy = copyField(field) + copy.careRuns[stage] = (copy.careRuns[stage] or 0) + 1 + copy.stage = stage + copy.startRequested = false + copy.retryAt = 0 + copy.reason = nil + return copy +end + +-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde. +function ADFieldModel.checkWidth(role, width, requiredWidth) if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH} end - if role ~= "mower" and mowWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < mowWidth then - return false, "tooNarrow", {width, mowWidth} + if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then + return false, "tooNarrow", {width, requiredWidth} end return true end -function ADFieldModel.wantsRig(field, assignedCount, now) +-- Hvilken rolle trinet mangler: {role, count, helper}. Hovedrollen sendes samlet (multi-tool kræver alle +-- baner), hjælpere (aflæsningsvogne) en ad gangen. counts = {primary = aktive, helpers = aktive}. +function ADFieldModel.wantsRig(field, counts, now) + if type(counts) == "number" then counts = {primary = counts, helpers = 0} end local role = ADFieldModel.roleFor(field.stage) - if role == nil or (field.retryAt or 0) > now or assignedCount >= ADFieldModel.maxRigs(field.stage) then - return nil + if role == nil or (field.retryAt or 0) > now then return nil end + local wanted = ADFieldModel.maxRigs(field.stage, field.plan) + if (counts.primary or 0) < wanted then + return {role = role, count = wanted - (counts.primary or 0), helper = false} end - return role + local helper = ADFieldModel.helperFor(field.stage) + if helper ~= nil and (counts.helpers or 0) < ADFieldModel.maxHelpers(field.stage, field.plan) then + return {role = helper, count = 1, helper = true} + end + return nil +end + +-- Hvilken afgrøde der skal sås: planens valg, ellers den afgrøde der blev høstet. +function ADFieldModel.sowFruit(field) + if field.plan.sowFruit ~= nil and field.plan.sowFruit ~= "" then return field.plan.sowFruit end + return field.lastFruit end function ADFieldModel.with(field, changes) @@ -164,6 +326,8 @@ function ADFieldModel.with(field, changes) for key, value in pairs(changes) do if key == "plan" then for planKey, planValue in pairs(value) do copy.plan[planKey] = planValue end + elseif value == false and (key == "reason") then + copy[key] = nil else copy[key] = value end @@ -171,15 +335,13 @@ function ADFieldModel.with(field, changes) return copy end --- Læssevognens Courseplay-forskydning når den kører rivens kursus: pickuppen skal ligge over skåret, der --- ligger windrowOffset m til venstre (positiv) for rivens kursuslinje. Courseplay tillader ±10 m. +-- Læssevognens Courseplay-forskydning når den kører rivens kursus (se etape 1-spec). function ADFieldModel.collectorOffset(ownOffset, windrowOffset) local offset = (ownOffset or 0) - (windrowOffset or 0) return math.max(-ADFieldModel.CP_MAX_TOOL_OFFSET, math.min(ADFieldModel.CP_MAX_TOOL_OFFSET, offset)) end --- Gæringssilo: en produktion der har markens skår som input og ensilage som output (FermentingSiloPack, --- GrassSilageSilo m.fl. er produktioner, ikke siloer). productions = {{inputs = {{type}}, outputs = {{type}}}}. +-- Gæringssilo: en produktion der har markens skår som input og ensilage som output. function ADFieldModel.isFermenter(productions, windrowFillType, silageFillType) if silageFillType == nil then return false end for _, production in ipairs(productions or {}) do @@ -195,10 +357,20 @@ function ADFieldModel.isFermenter(productions, windrowFillType, silageFillType) return false end +local function flag(value) return value and 1 or 0 end + function ADFieldModel.toRecord(field) + local plan = field.plan + local care = {} + for stage, runs in pairs(field.careRuns or {}) do table.insert(care, stage .. "=" .. tostring(runs)) end + table.sort(care) return { - id = field.id, stage = field.stage, mode = field.plan.mode, product = field.plan.product, - rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "), + id = field.id, stage = field.stage, mode = plan.mode, product = plan.product, + rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "), + rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime), + soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll), + fertilize = flag(plan.fertilize), weed = flag(plan.weed), + kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "), mowWidth = field.mowWidth or 0, mowVehicle = field.mowVehicleId or "", rakeVehicle = field.rakeVehicleId or "", @@ -206,16 +378,40 @@ function ADFieldModel.toRecord(field) } end +local function readFlag(value, default) + if value == nil then return default end + return value == 1 or value == true +end + +local function readText(value) + return value ~= nil and value ~= "" and value or nil +end + function ADFieldModel.fromRecord(record) local field = ADFieldModel.newField(record.id) + local plan = field.plan field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle" - field.plan.mode = record.mode == "auto" and "auto" or "manual" - field.plan.product = record.product == "hay" and "hay" or "silage" - field.plan.rake = record.rake == 1 or record.rake == true - for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end + plan.mode = record.mode == "auto" and "auto" or "manual" + -- etape 1 gemte "silage"/"hay"; ukendt -> auto + plan.product = PRODUCTS[record.product] and record.product or "auto" + plan.rake = readFlag(record.rake, false) + for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end + plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS) + plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS) + plan.mulch = readFlag(record.mulch, false) + plan.lime = readFlag(record.lime, true) + plan.soil = SOIL_VALUES[record.soil] and record.soil or "auto" + plan.sow = readFlag(record.sow, false) + plan.sowFruit = record.sowFruit or "" + plan.roll = readFlag(record.roll, false) + plan.fertilize = readFlag(record.fertilize, true) + plan.weed = readFlag(record.weed, true) + field.kind = readText(record.kind) + field.lastFruit = readText(record.lastFruit) + for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil - field.mowVehicleId = record.mowVehicle ~= nil and record.mowVehicle ~= "" and record.mowVehicle or nil - field.rakeVehicleId = record.rakeVehicle ~= nil and record.rakeVehicle ~= "" and record.rakeVehicle or nil + field.mowVehicleId = readText(record.mowVehicle) + field.rakeVehicleId = readText(record.rakeVehicle) field.windrowOffset = tonumber(record.windrowOffset) return field end diff --git a/tests/test_adFieldModel.lua b/tests/test_adFieldModel.lua index a762b01..ebaaebe 100644 --- a/tests/test_adFieldModel.lua +++ b/tests/test_adFieldModel.lua @@ -13,7 +13,9 @@ end local f = M.newField(109) check("M1 stage", f.stage, "idle") check("M1 mode", f.plan.mode, "manual") -check("M1 product", f.plan.product, "silage") +check("M1 product auto", f.plan.product, "auto") +check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage") +check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest") check("M1 rake", f.plan.rake, false) -- M2: klar ved >= 90 % @@ -60,7 +62,7 @@ check("M6 stop -> klar", M.stop(started).stage, "ready") check("M7 rolle slå", M.roleFor("mowing"), "mower") check("M7 rolle saml", M.roleFor("collecting"), "collector") check("M7 rolle klar = ingen", M.roleFor("ready"), nil) -check("M7 maks læssevogne", M.maxRigs("collecting"), 2) +check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2) check("M7 maks slå", M.maxRigs("mowing"), 1) local okWide, whyWide = M.checkWidth("mower", 60, nil) check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide") @@ -71,11 +73,11 @@ check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), t check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true) -- wantsRig: kun arbejdende trin, under maks, efter pause -check("M7 wantsRig slå", M.wantsRig(started, 0, 0), "mower") +check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower") check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil) local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000 check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil) -check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000), "mower") +check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower") check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil) -- M8: gem/indlæs bevarer trin, plan og slåbredde @@ -98,7 +100,7 @@ local base = M.start(M.onReadiness(M.newField(1), 1)) local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}}) check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5) check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual") -check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "silage") +check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto") -- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready") @@ -142,5 +144,88 @@ check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false) check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false) +-- M14: høstkæde + klargøring efter behov +local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true, + soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}} +check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling") +check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling") +local noSoil = {soil = "none", sow = false, lime = false, mulch = true} +check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching") +check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling") +check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing") +local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {}) +check("M14 start = høst", harvestField.stage, "harvesting") +check("M14 husker art", harvestField.kind, "harvest") +local afterHarvest = M.advance(harvestField, {}) +check("M14 høst -> kultivér", afterHarvest.stage, "cultivating") +check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing") +check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done") +check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating") +check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done") + +-- M15: hjælpere under høst og multi-tool-baner +check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader") +check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true) +check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil) +local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}}) +check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3) +check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2) +check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0) +check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1") +check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1") +check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2") +check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2") +check("M15 over 5 -> 5", #M.lanePositions(9), 5) +check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5) + +-- M16: pleje under vækst +local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0} +local idleField = M.newField(30) +check("M16 gødning", M.careNeed(idleField, growing), "fertilizing") +check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding") +check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil) +check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming") +check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil) +check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil) +check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil) +check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil) +local cared = M.startCare(idleField, "fertilizing") +check("M16 pleje-trin", cared.stage, "fertilizing") +check("M16 rolle", M.roleFor(cared.stage), "fertilizer") +check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer") +check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle") +local twice = M.startCare(M.advance(cared, {}), "fertilizing") +check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil) +check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true) +check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil) +check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle") +check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready") + +-- M17: såafgrøde og gem/indlæs af den udvidede plan +check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT") +check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA") +local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY", + plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}}) +local round = M.fromRecord(M.toRecord(full)) +check("M17 trin", round.stage, "weeding") +check("M17 sæt", round.plan.rigs, 3) +check("M17 vogne", round.plan.unloaders, 2) +check("M17 stub", round.plan.mulch, true) +check("M17 kalk fra", round.plan.lime, false) +check("M17 jord", round.plan.soil, "plow") +check("M17 så", round.plan.sow, true) +check("M17 såafgrøde", round.plan.sowFruit, "WHEAT") +check("M17 tromle", round.plan.roll, true) +check("M17 gødning fra", round.plan.fertilize, false) +check("M17 ukrudt fra", round.plan.weed, false) +check("M17 art", round.kind, "harvest") +check("M17 sidste afgrøde", round.lastFruit, "BARLEY") +check("M17 pleje-tæller", round.careRuns.weeding, 1) +check("M17 produkt", round.plan.product, "harvest") +local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1}) +check("M17 etape 1-post: hø", legacy.plan.product, "hay") +check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true) +check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)