breedAuto_cows.lua

619 lines · 16.8 KB

Open raw

Cow Auto-Breeder (Perimeter Walker)

Copy & run

wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/husbandry/breedAuto_cows.lua
1-- Cow Auto-Breeder (Perimeter Walker)
2-- Requirements: CC:Tweaked turtle with Advanced Peripherals Husbandry Automata upgrade
3-- Place a chest directly behind the turtle at start for refuel/restock/deposit
4
5-- ===================== UI HELPERS =====================
6local function supportsColor()
7 return term.isColor and term.isColor()
8end
9
10local function clearScreen()
11 term.clear()
12 term.setCursorPos(1, 1)
13end
14
15local function banner()
16 if supportsColor() then term.setTextColor(colors.lime) end
17 print("==============================")
18 print(" Cow Auto-Breeder - Perimeter ")
19 print("==============================")
20 if supportsColor() then term.setTextColor(colors.white) end
21end
22
23local function prompt(text, default)
24 io.write(text)
25 if default ~= nil then io.write(" [" .. tostring(default) .. "]") end
26 io.write(": ")
27 local ans = read()
28 ans = ans and ans:gsub("^%s+", ""):gsub("%s+$", "") or ""
29 if ans == "" and default ~= nil then return default end
30 return ans
31end
32
33local function waitEnter(msg)
34 io.write(msg or "Press Enter to continue...")
35 read()
36end
37
38-- ===================== PERIPHERAL =====================
39local function findHusbandryPeripheral()
40 local candidates = {"husbandry_automata", "husbandryAutomata", "weak_automata", "automata"}
41 for _, name in ipairs(candidates) do
42 local p = peripheral.find(name)
43 if p then return p, name end
44 end
45 return nil, nil
46end
47
48-- ===================== CONFIG =====================
49local Config = {
50 fenceSampleSlot = 1,
51 cooldownSeconds = 300,
52 maxCows = 24,
53 cullingEnabled = true,
54 targetWheat = 64,
55 minFuel = 1000,
56}
57
58local function runSetup()
59 clearScreen()
60 banner()
61 print("Setup:")
62 print("- Ensure a chest is placed DIRECTLY BEHIND the turtle.")
63 print("- Put wheat and coal/charcoal into that chest for restocking/refuel.")
64 print("- Place the turtle INSIDE the pen, facing along the fence so the fence is on its RIGHT.")
65 print("")
66 print("Provide a sample of the pen's fence block in an inventory slot.")
67 Config.fenceSampleSlot = tonumber(prompt("Which slot holds the fence sample?", tostring(Config.fenceSampleSlot))) or Config.fenceSampleSlot
68 Config.maxCows = tonumber(prompt("Max adult cows to keep (cull if above)", tostring(Config.maxCows))) or Config.maxCows
69 Config.cullingEnabled = string.lower(prompt("Enable culling if above max? (yes/no)", Config.cullingEnabled and "yes" or "no")) == "yes"
70 local mins = tonumber(prompt("Minutes between breeding cycles", tostring(math.floor(Config.cooldownSeconds / 60)))) or (Config.cooldownSeconds / 60)
71 Config.cooldownSeconds = math.floor(mins * 60)
72 Config.targetWheat = tonumber(prompt("Target wheat count to keep in inventory", tostring(Config.targetWheat))) or Config.targetWheat
73 Config.minFuel = tonumber(prompt("Minimum fuel level to maintain", tostring(Config.minFuel))) or Config.minFuel
74 print("")
75 print("Setup complete. Starting...")
76 os.sleep(1)
77end
78
79-- ===================== INVENTORY =====================
80local function getItemDetail(slot)
81 local d = turtle.getItemDetail(slot)
82 if d then
83 return {
84 slot = slot,
85 name = d.name,
86 displayName = d.displayName or d.name,
87 count = d.count or turtle.getItemCount(slot),
88 }
89 end
90 return nil
91end
92
93local function eachItem()
94 local i = 0
95 return function()
96 i = i + 1
97 while i <= 16 do
98 local d = getItemDetail(i)
99 if d then return d end
100 i = i + 1
101 end
102 return nil
103 end
104end
105
106local function itemMatches(detail, keywords)
107 local lname = string.lower((detail.name or "") .. " " .. (detail.displayName or ""))
108 for _, key in ipairs(keywords) do
109 if string.find(lname, string.lower(key), 1, true) then return true end
110 end
111 return false
112end
113
114local function selectItem(keywords)
115 for item in eachItem() do
116 if itemMatches(item, keywords) then
117 turtle.select(item.slot)
118 return true, item
119 end
120 end
121 return false
122end
123
124local function countItems(keywords)
125 local total = 0
126 for item in eachItem() do
127 if itemMatches(item, keywords) then total = total + item.count end
128 end
129 return total
130end
131
132local function dropAllExcept(keepKeywords)
133 -- Face chest assumed in front
134 for slot = 1, 16 do
135 local d = getItemDetail(slot)
136 if d then
137 local keep = itemMatches(d, keepKeywords)
138 if not keep then
139 turtle.select(slot)
140 turtle.drop()
141 end
142 end
143 end
144end
145
146local function suckUntil(predicate, maxActions)
147 maxActions = maxActions or 64
148 local actions = 0
149 while actions < maxActions do
150 if predicate() then return true end
151 if not turtle.suck() then return predicate() end
152 actions = actions + 1
153 end
154 return predicate()
155end
156
157local COAL_KEYS = {"coal", "charcoal"}
158local WHEAT_KEYS = {"wheat"}
159local SWORD_KEYS = {"sword"}
160
161local function faceChest(fn)
162 -- Turn around, do fn(), turn back
163 turtle.turnLeft(); turtle.turnLeft()
164 local ok, err = pcall(fn)
165 turtle.turnLeft(); turtle.turnLeft()
166 if not ok then error(err) end
167end
168
169local function ensureFuel(minFuel)
170 local lvl = turtle.getFuelLevel()
171 if lvl == "unlimited" or lvl == math.huge then return true end
172 if lvl >= minFuel then return true end
173 faceChest(function()
174 -- Try to refuel using existing coal first
175 if not selectItem(COAL_KEYS) then
176 -- Pull from chest
177 suckUntil(function() return selectItem(COAL_KEYS) end, 128)
178 end
179 if selectItem(COAL_KEYS) then
180 -- Refuel until target
181 for _ = 1, 16 do
182 local before = turtle.getFuelLevel()
183 turtle.refuel(1)
184 if turtle.getFuelLevel() >= minFuel then break end
185 if turtle.getFuelLevel() == before then break end
186 end
187 end
188 -- Drop back non-keep
189 dropAllExcept({"wheat", "coal", "charcoal"})
190 end)
191 return turtle.getFuelLevel() == "unlimited" or turtle.getFuelLevel() >= minFuel
192end
193
194local function ensureWheat(targetCount)
195 if countItems(WHEAT_KEYS) >= targetCount then return true end
196 faceChest(function()
197 -- pull until we reach target or chest empty
198 suckUntil(function() return countItems(WHEAT_KEYS) >= targetCount end, 256)
199 -- drop back everything except wheat and coal
200 dropAllExcept({"wheat", "coal", "charcoal"})
201 end)
202 return countItems(WHEAT_KEYS) >= math.min(1, targetCount)
203end
204
205local function depositNonKeep()
206 faceChest(function()
207 dropAllExcept({"wheat", "coal", "charcoal"})
208 end)
209end
210
211-- ===================== MOVEMENT / PERIMETER =====================
212local heading = 0 -- 0=N,1=E,2=S,3=W
213local startHeading = 0
214local posX, posZ = 0, 0
215local moveHistory = {}
216
217local function setHeading(h)
218 heading = (h % 4 + 4) % 4
219end
220
221local function turnRight()
222 turtle.turnRight()
223 setHeading(heading + 1)
224 moveHistory[#moveHistory + 1] = "R"
225end
226
227local function turnLeft()
228 turtle.turnLeft()
229 setHeading(heading - 1)
230 moveHistory[#moveHistory + 1] = "L"
231end
232
233local function turnRightNoRecord()
234 turtle.turnRight()
235 setHeading(heading + 1)
236end
237
238local function turnLeftNoRecord()
239 turtle.turnLeft()
240 setHeading(heading - 1)
241end
242
243local function forward()
244 if turtle.forward() then
245 if heading == 0 then posZ = posZ - 1
246 elseif heading == 1 then posX = posX + 1
247 elseif heading == 2 then posZ = posZ + 1
248 else posX = posX - 1 end
249 moveHistory[#moveHistory + 1] = "F"
250 return true
251 end
252 return false
253end
254
255local function moveBack()
256 if turtle.back() then
257 if heading == 0 then posZ = posZ + 1
258 elseif heading == 1 then posX = posX - 1
259 elseif heading == 2 then posZ = posZ - 1
260 else posX = posX + 1 end
261 moveHistory[#moveHistory + 1] = "B"
262 return true
263 end
264 return false
265end
266
267local function resetPose()
268 posX, posZ = 0, 0
269 startHeading = heading
270 moveHistory = {}
271end
272
273local fenceName = nil
274local function readFenceNameFromSample()
275 local d = getItemDetail(Config.fenceSampleSlot)
276 if not d then return nil end
277 return d.name
278end
279
280local function inspectFrontName()
281 local ok, data = turtle.inspect()
282 if ok and data and data.name then return data.name end
283 return nil
284end
285
286local function hasFenceRight()
287 turnRightNoRecord()
288 local name = inspectFrontName()
289 turnLeftNoRecord()
290 return name == fenceName
291end
292
293local function frontBlockedByBlock()
294 return turtle.detect()
295end
296
297-- Forward declarations for functions referenced before definition
298local tryFeedFrontCow
299local tryCullFrontCow
300
301local function isFenceAhead()
302 if not frontBlockedByBlock() then return false end
303 return inspectFrontName() == fenceName
304end
305
306local function turnToHeading(target)
307 target = (target % 4 + 4) % 4
308 local diff = (target - heading) % 4
309 if diff == 1 then
310 turnRight()
311 elseif diff == 2 then
312 turnRight(); turnRight()
313 elseif diff == 3 then
314 turnLeft()
315 end
316end
317
318local function advanceToWallCurrentHeading(mode)
319 local guard = 0
320 while not isFenceAhead() and guard < 8192 do
321 if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end
322 local ok, reason = forceForward(mode, 30)
323 if not ok and reason == "fence" then break end
324 guard = guard + 1
325 end
326end
327
328-- ===================== ANIMAL INTERACTION =====================
329local automata, automataName = nil, nil
330
331local function isCowEntityInfo(info)
332 if not info then return false end
333 local vals = {
334 string.lower(tostring(info.id or "")),
335 string.lower(tostring(info.name or "")),
336 string.lower(tostring(info.species or "")),
337 }
338 for _, v in ipairs(vals) do
339 if v ~= "" and (v == "minecraft:cow" or v == "cow" or v:find("cow", 1, true)) then return true end
340 end
341 return false
342end
343
344local function isBabyFromInfo(info)
345 if not info then return false end
346 if info.isBaby == true then return true end
347 if type(info.age) == "number" and info.age < 0 then return true end
348 if type(info.growingAge) == "number" and info.growingAge < 0 then return true end
349 return false
350end
351
352tryFeedFrontCow = function()
353 local info = select(1, automata.inspectAnimal())
354 if not info or not isCowEntityInfo(info) then return false, "no_cow" end
355 if isBabyFromInfo(info) then return false, "baby" end
356 if not selectItem(WHEAT_KEYS) then return false, "no_wheat" end
357 local ok, msg = automata.useOnAnimal()
358 return ok or false, msg
359end
360
361tryCullFrontCow = function()
362 local info = select(1, automata.inspectAnimal())
363 if not info or not isCowEntityInfo(info) then return false, "no_cow" end
364 if isBabyFromInfo(info) then return false, "baby" end
365 -- Attempt a few attacks
366 selectItem(SWORD_KEYS)
367 local attacked = false
368 for _ = 1, 5 do
369 if turtle.attack() then attacked = true end
370 os.sleep(0.1)
371 -- if path clears, break
372 if forward() then
373 -- stepped into spot; step back to maintain path
374 moveBack()
375 break
376 end
377 end
378 return attacked, attacked and "attacked" or "blocked"
379end
380
381-- Robust movement that forces progress through entities (not blocks)
382local function forceForward(mode, maxTries)
383 maxTries = maxTries or 30
384 if frontBlockedByBlock() and inspectFrontName() == fenceName then
385 return false, "fence"
386 end
387 for i = 1, maxTries do
388 if forward() then return true end
389 -- entity in front; interact per mode
390 if mode == "breed" then
391 tryFeedFrontCow()
392 else
393 tryCullFrontCow()
394 end
395 os.sleep(0.1)
396 end
397 return false, "entity_blocked"
398end
399
400local function forceBack(maxTries)
401 maxTries = maxTries or 20
402 for i = 1, maxTries do
403 if moveBack() then return true end
404 -- fallback: 180 and forward
405 turnLeftNoRecord(); turnLeftNoRecord()
406 local ok = forward()
407 turnLeftNoRecord(); turnLeftNoRecord()
408 if ok then return true end
409 os.sleep(0.05)
410 end
411 return false
412end
413
414local function countNearbyCows()
415 local list = select(1, automata.searchAnimals()) or {}
416 local countAdult = 0
417 for _, a in ipairs(list) do
418 if isCowEntityInfo(a) then
419 if not isBabyFromInfo(a) then countAdult = countAdult + 1 end
420 end
421 end
422 return countAdult
423end
424
425-- ===================== PERIMETER WALK =====================
426local function orientToFenceRight(maxTries)
427 maxTries = maxTries or 4
428 for _ = 1, maxTries do
429 if hasFenceRight() then return true end
430 turnRight()
431 end
432 return false
433end
434
435local function handleEntityBlock(mode)
436 -- try to feed/attack then attempt to move forward a few times
437 if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end
438 for _ = 1, 6 do
439 if forward() then return true end
440 os.sleep(0.1)
441 end
442 return false
443end
444
445-- Stable right-hand wall follower around the specified fence
446local function perimeterOnce(mode)
447 mode = mode or "breed" -- or "cull"
448 -- Ensure we start hugging fence on right
449 if not orientToFenceRight(8) then
450 print("Could not find fence on the right. Adjust turtle position/orientation and press Enter.")
451 waitEnter()
452 if not orientToFenceRight(8) then return false, "no_fence" end
453 end
454 resetPose()
455 local steps = 0
456 local safety = 16000
457 while safety > 0 do
458 safety = safety - 1
459 -- Interact opportunistically if a cow is in front
460 if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end
461
462 local rightIsFence = hasFenceRight()
463 local frontIsBlock = frontBlockedByBlock()
464 if not rightIsFence then
465 -- no fence on right: turn right and try to go forward
466 turnRight()
467 if frontBlockedByBlock() then
468 -- corner or obstacle, turn left back to continue search
469 turnLeft()
470 else
471 if not forward() then
472 -- entity blocking
473 handleEntityBlock(mode)
474 end
475 end
476 else
477 -- fence is on the right; follow along it
478 if frontIsBlock then
479 -- turn left around corner
480 turnLeft()
481 else
482 if not forward() then
483 -- entity blocking
484 handleEntityBlock(mode)
485 end
486 end
487 end
488 steps = steps + 1
489 -- Loop complete if back to start pose after some movement
490 if posX == 0 and posZ == 0 and heading == startHeading and steps > 8 then
491 return true
492 end
493 -- light delay to be friendly
494 os.sleep(0.05)
495 end
496 return false, "safety_exceeded"
497end
498
499-- Serpentine traversal within the pen. Returns to home using move history.
500local function serpentineOnce(mode)
501 mode = mode or "breed"
502 resetPose()
503
504 -- Step 1: go straight until first wall in current facing
505 local dirFirst = heading
506 advanceToWallCurrentHeading(mode)
507
508 -- Step 2: turn right and go to the next wall (corner)
509 turnRight()
510 local dirSecond = heading
511 advanceToWallCurrentHeading(mode)
512
513 -- Define run and lateral directions relative to first/second legs
514 local runDir = (dirSecond + 2) % 4 -- opposite of second leg (go away from that wall)
515 local lateralDir = (dirFirst + 2) % 4 -- move one block opposite of first leg between runs
516
517 -- Serpentine columns: run to wall, shift 1 laterally, reverse run, repeat
518 local flips = 0
519 local safety = 20000
520 while safety > 0 do
521 safety = safety - 1
522 -- Run to wall along runDir
523 turnToHeading(runDir)
524 advanceToWallCurrentHeading(mode)
525
526 -- Shift one block laterally; stop if fence blocks shift
527 turnToHeading(lateralDir)
528 if isFenceAhead() then break end
529 forceForward(mode, 30)
530
531 -- Reverse run direction for next column
532 runDir = (runDir + 2) % 4
533 flips = flips + 1
534 if flips > 8192 then break end
535 end
536
537 -- Go home by reversing history
538 for i = #moveHistory, 1, -1 do
539 local op = moveHistory[i]
540 if op == "F" then
541 forceBack(15)
542 elseif op == "B" then
543 forceForward("breed", 15)
544 elseif op == "L" then
545 turnRightNoRecord()
546 elseif op == "R" then
547 turnLeftNoRecord()
548 end
549 end
550 moveHistory = {}
551 return true
552end
553
554-- ===================== MAIN LOOP =====================
555local function main()
556 if not turtle then
557 error("Must run on a turtle")
558 end
559 automata, automataName = findHusbandryPeripheral()
560 if not automata then
561 error("Husbandry Automata peripheral not found")
562 end
563 runSetup()
564 fenceName = readFenceNameFromSample()
565 if not fenceName then error("Fence sample slot is empty or invalid") end
566
567 -- Initialize pose tracking on demand (perimeterOnce resets pose)
568
569 while true do
570 clearScreen(); banner()
571 print("Servicing chest and preparing...")
572 depositNonKeep()
573 ensureWheat(Config.targetWheat)
574 ensureFuel(Config.minFuel)
575
576 print("Breeding serpentine pass...")
577 local ok, err = serpentineOnce("breed")
578 if not ok then print("Traversal error: " .. tostring(err)) end
579
580 -- Wait cooldown BEFORE culling
581 local waitSec = Config.cooldownSeconds
582 print("Waiting " .. tostring(waitSec) .. "s before next cycle...")
583 for s = waitSec, 1, -1 do
584 term.setCursorPos(1, 10)
585 print(string.format("Next in %ds ", s))
586 os.sleep(1)
587 end
588
589 -- Culling pass after waiting
590 local count = countNearbyCows()
591 print("Detected adult cows: " .. tostring(count))
592 if Config.cullingEnabled and count > Config.maxCows then
593 print("Culling serpentine pass to target <= " .. tostring(Config.maxCows))
594 local safety = 12
595 while count > Config.maxCows and safety > 0 do
596 local okCull = select(1, serpentineOnce("cull"))
597 count = countNearbyCows()
598 print("Cows remaining: " .. tostring(count))
599 safety = safety - 1
600 if not okCull then break end
601 end
602 end
603
604 print("Depositing drops and refueling/restocking...")
605 depositNonKeep()
606 ensureFuel(Config.minFuel)
607 ensureWheat(Config.targetWheat)
608 end
609end
610
611local ok, err = pcall(main)
612if not ok then
613 print("Error: " .. tostring(err))
614 warn = warn or print
615 warn("Program terminated.")
616end
617
618
619