· last year · Sep 29, 2024, 10:35 PM
1-- Turtle Auto-Sorting and Mapping Program with A* Pathfinding and User Data Choice
2-- Author: [Your Name]
3-- Description:
4-- This program enables a turtle to explore your house, map it,
5-- navigate around obstacles using A* pathfinding, identify chests,
6-- categorize them based on contents, and redistribute items to the appropriate chests.
7-- It includes enhanced navigation, mapping, logging, user input, and error handling.
8
9-- Ensure required APIs are available
10if not fs.exists("json") then
11 print("Downloading JSON API...")
12 shell.run("pastebin get 4nRg9CHU json")
13end
14
15os.loadAPI("json")
16
17-- Configuration Variables
18local logFileName = "turtle_log.txt"
19local chestDataFile = "chest_data.txt"
20local mapDataFile = "map_data.txt"
21local areaSize = 50 -- Default area size; can be changed via user input
22local fuelThreshold = 500 -- Turtle will refuel when fuel level drops below this value
23local mappingEnabled = true -- Enable mapping and advanced navigation
24
25-- Initialize Variables
26local position = {x = 0, y = 0, z = 0}
27local direction = 0 -- 0: east, 1: south, 2: west, 3: north
28local map = {} -- 3D table for mapping
29local chestData = {}
30local itemCategories = {}
31local turtleInventorySize = 16
32
33-- User choices
34local loadExistingMapData = true
35local loadExistingChestData = true
36
37-- Predeclare functions to avoid 'nil value' errors
38local posKey
39local navigateTo
40local findPath
41local processChests
42local findChestForItem
43local refuel
44local detectAndUpdate
45local markObstacle
46
47-- Load or Initialize Map Data
48local function loadMapData()
49 if fs.exists(mapDataFile) then
50 local file = fs.open(mapDataFile, "r")
51 local data = file.readAll()
52 file.close()
53 map = json.decode(data)
54 else
55 map = {}
56 end
57end
58
59local function saveMapData()
60 local file = fs.open(mapDataFile, "w")
61 file.write(json.encode(map))
62 file.close()
63end
64
65-- Load or Initialize Chest Data
66local function loadChestData()
67 if fs.exists(chestDataFile) then
68 local file = fs.open(chestDataFile, "r")
69 local data = file.readAll()
70 file.close()
71 chestData = json.decode(data)
72 else
73 chestData = {}
74 end
75end
76
77local function saveChestData()
78 local file = fs.open(chestDataFile, "w")
79 file.write(json.encode(chestData))
80 file.close()
81end
82
83-- Logging Functions
84local function log(message)
85 local file = fs.open(logFileName, "a")
86 file.writeLine(os.date("%Y-%m-%d %H:%M:%S") .. " - " .. message)
87 file.close()
88end
89
90local function logError(errorMessage)
91 log("ERROR: " .. errorMessage)
92end
93
94-- User Input for Configuration
95local function getUserInput()
96 print("Enter the size of the area to scan (default 50):")
97 local input = read()
98 local num = tonumber(input)
99 if num and num > 0 then
100 areaSize = num
101 else
102 print("Invalid input. Using default size of 50.")
103 log("Invalid area size input. Using default.")
104 end
105
106 print("Do you want to load existing map data? (yes/no):")
107 input = read()
108 if input:lower() == "yes" or input:lower() == "y" then
109 loadExistingMapData = true
110 else
111 loadExistingMapData = false
112 -- Optionally, delete existing map data file
113 if fs.exists(mapDataFile) then
114 fs.delete(mapDataFile)
115 log("Deleted existing map data.")
116 end
117 end
118
119 print("Do you want to load existing chest data? (yes/no):")
120 input = read()
121 if input:lower() == "yes" or input:lower() == "y" then
122 loadExistingChestData = true
123 else
124 loadExistingChestData = false
125 -- Optionally, delete existing chest data file
126 if fs.exists(chestDataFile) then
127 fs.delete(chestDataFile)
128 log("Deleted existing chest data.")
129 end
130 end
131end
132
133-- Define Item Categories
134local function initializeItemCategories()
135 itemCategories = {
136 ores = {
137 "minecraft:iron_ore",
138 "minecraft:gold_ore",
139 "minecraft:coal_ore",
140 "minecraft:diamond_ore",
141 "minecraft:emerald_ore",
142 "minecraft:redstone_ore",
143 "minecraft:lapis_ore",
144 "minecraft:nether_quartz_ore",
145 },
146 smelted_ores = {
147 "minecraft:iron_ingot",
148 "minecraft:gold_ingot",
149 "minecraft:diamond",
150 "minecraft:emerald",
151 "minecraft:redstone",
152 "minecraft:lapis_lazuli",
153 "minecraft:quartz",
154 },
155 potions = {
156 "minecraft:potion",
157 "minecraft:splash_potion",
158 "minecraft:lingering_potion",
159 },
160 farming = {
161 "minecraft:wheat",
162 "minecraft:carrot",
163 "minecraft:potato",
164 "minecraft:beetroot",
165 "minecraft:melon_slice",
166 "minecraft:pumpkin",
167 "minecraft:sugar_cane",
168 "minecraft:cocoa_beans",
169 "minecraft:nether_wart",
170 },
171 tools = {
172 "minecraft:wooden_pickaxe",
173 "minecraft:stone_pickaxe",
174 "minecraft:iron_pickaxe",
175 "minecraft:golden_pickaxe",
176 "minecraft:diamond_pickaxe",
177 "minecraft:netherite_pickaxe",
178 "minecraft:wooden_sword",
179 "minecraft:stone_sword",
180 "minecraft:iron_sword",
181 "minecraft:golden_sword",
182 "minecraft:diamond_sword",
183 "minecraft:netherite_sword",
184 "minecraft:bow",
185 "minecraft:crossbow",
186 "minecraft:shield",
187 },
188 junk = {
189 "minecraft:rotten_flesh",
190 "minecraft:string",
191 "minecraft:spider_eye",
192 "minecraft:gunpowder",
193 "minecraft:bone",
194 "minecraft:arrow",
195 "minecraft:flint",
196 },
197 miscellaneous = {
198 -- Items that don't fit in other categories
199 },
200 }
201end
202
203-- Utility Functions
204local function isItemInCategory(itemName, categoryItems)
205 for _, name in pairs(categoryItems) do
206 if name == itemName then
207 return true
208 end
209 end
210 return false
211end
212
213local function getItemCategory(itemName)
214 for category, itemList in pairs(itemCategories) do
215 if isItemInCategory(itemName, itemList) then
216 return category
217 end
218 end
219 return "miscellaneous"
220end
221
222local function determineChestCategory(items)
223 local categoryCount = {}
224 for category, _ in pairs(itemCategories) do
225 categoryCount[category] = 0
226 end
227
228 for _, item in pairs(items) do
229 local category = getItemCategory(item.name)
230 categoryCount[category] = categoryCount[category] + item.count
231 end
232
233 -- Find the category with the highest count
234 local maxCount = 0
235 local chestCategory = "miscellaneous"
236 for category, count in pairs(categoryCount) do
237 if count > maxCount then
238 maxCount = count
239 chestCategory = category
240 end
241 end
242
243 return chestCategory
244end
245
246-- Map Functions
247local function setCellStatus(x, y, z, status)
248 map[x] = map[x] or {}
249 map[x][y] = map[x][y] or {}
250 map[x][y][z] = status
251end
252
253local function getCellStatus(x, y, z)
254 if map[x] and map[x][y] and map[x][y][z] then
255 return map[x][y][z]
256 else
257 return "unknown"
258 end
259end
260
261-- Movement Functions
262local function turnRight()
263 turtle.turnRight()
264 direction = (direction + 1) % 4
265end
266
267local function turnLeft()
268 turtle.turnLeft()
269 direction = (direction - 1) % 4
270 if direction < 0 then direction = direction + 4 end
271end
272
273local function faceDirection(targetDirection)
274 while direction ~= targetDirection do
275 turnRight()
276 end
277end
278
279local function moveForward()
280 if turtle.getFuelLevel() < fuelThreshold then
281 refuel()
282 end
283 if turtle.forward() then
284 if direction == 0 then
285 position.x = position.x + 1
286 elseif direction == 1 then
287 position.z = position.z + 1
288 elseif direction == 2 then
289 position.x = position.x - 1
290 elseif direction == 3 then
291 position.z = position.z - 1
292 end
293 detectAndUpdate()
294 return true
295 else
296 markObstacle()
297 return false
298 end
299end
300
301local function moveUp()
302 if turtle.getFuelLevel() < fuelThreshold then
303 refuel()
304 end
305 if turtle.up() then
306 position.y = position.y + 1
307 detectAndUpdate()
308 return true
309 else
310 return false
311 end
312end
313
314local function moveDown()
315 if turtle.getFuelLevel() < fuelThreshold then
316 refuel()
317 end
318 if turtle.down() then
319 position.y = position.y - 1
320 detectAndUpdate()
321 return true
322 else
323 return false
324 end
325end
326
327-- Fuel Management
328function refuel()
329 local initialFuel = turtle.getFuelLevel()
330 for slot = 1, turtleInventorySize do
331 local item = turtle.getItemDetail(slot)
332 if item then
333 turtle.select(slot)
334 if turtle.refuel(0) then
335 local fuelToConsume = math.min(item.count, math.ceil((fuelThreshold - turtle.getFuelLevel()) / 80))
336 if fuelToConsume > 0 then
337 if turtle.refuel(fuelToConsume) then
338 return true
339 end
340 end
341 end
342 end
343 end
344
345 if turtle.getFuelLevel() > initialFuel then
346 return true
347 else
348 return false
349 end
350end
351
352-- Obstacle Handling
353function detectAndUpdate()
354 -- Update the map in front of the turtle
355 local dx, dz = 0, 0
356 if direction == 0 then dx = 1
357 elseif direction == 1 then dz = 1
358 elseif direction == 2 then dx = -1
359 elseif direction == 3 then dz = -1 end
360
361 local x, z = position.x + dx, position.z + dz
362 local success, data = turtle.inspect()
363 if success then
364 if data.name == "minecraft:chest" then
365 setCellStatus(x, position.y, z, "chest")
366 else
367 setCellStatus(x, position.y, z, "wall")
368 end
369 else
370 setCellStatus(x, position.y, z, "empty")
371 end
372 saveMapData()
373end
374
375function markObstacle()
376 local dx, dz = 0, 0
377 if direction == 0 then dx = 1
378 elseif direction == 1 then dz = 1
379 elseif direction == 2 then dx = -1
380 elseif direction == 3 then dz = -1 end
381
382 local x, z = position.x + dx, position.z + dz
383 setCellStatus(x, position.y, z, "wall")
384end
385
386-- A* Pathfinding Functions
387
388-- Priority Queue implementation for the A* algorithm
389local function priorityQueue()
390 local self = {}
391 self.heap = {}
392
393 function self:push(node, priority)
394 table.insert(self.heap, {node = node, priority = priority})
395 self:_siftUp(#self.heap)
396 end
397
398 function self:pop()
399 if #self.heap == 0 then return nil end
400 local root = self.heap[1].node
401 self.heap[1] = self.heap[#self.heap]
402 table.remove(self.heap)
403 self:_siftDown(1)
404 return root
405 end
406
407 function self:isEmpty()
408 return #self.heap == 0
409 end
410
411 function self:_siftUp(index)
412 while index > 1 do
413 local parent = math.floor(index / 2)
414 if self.heap[parent].priority <= self.heap[index].priority then
415 break
416 end
417 self.heap[parent], self.heap[index] = self.heap[index], self.heap[parent]
418 index = parent
419 end
420 end
421
422 function self:_siftDown(index)
423 while true do
424 local left = index * 2
425 local right = index * 2 + 1
426 local smallest = index
427
428 if left <= #self.heap and self.heap[left].priority < self.heap[smallest].priority then
429 smallest = left
430 end
431 if right <= #self.heap and self.heap[right].priority < self.heap[smallest].priority then
432 smallest = right
433 end
434 if smallest == index then break end
435 self.heap[smallest], self.heap[index] = self.heap[index], self.heap[smallest]
436 index = smallest
437 end
438 end
439
440 return self
441end
442
443-- Heuristic function for A* (Manhattan distance)
444local function heuristic(a, b)
445 return math.abs(a.x - b.x) + math.abs(a.z - b.z)
446end
447
448-- Key function for positions
449function posKey(pos)
450 return pos.x .. "," .. pos.y .. "," .. pos.z
451end
452
453-- A* Pathfinding function with an emphasis on exploration
454function findPath(startPos, endPos)
455 local openSet = priorityQueue()
456 openSet:push(startPos, 0)
457
458 local cameFrom = {}
459 local gScore = {}
460 local fScore = {}
461
462 local startKey = posKey(startPos)
463 local endKey = posKey(endPos)
464
465 gScore[startKey] = 0
466 fScore[startKey] = heuristic(startPos, endPos)
467
468 local openSetNodes = {}
469 openSetNodes[startKey] = true
470
471 while not openSet:isEmpty() do
472 local current = openSet:pop()
473 local currentKey = posKey(current)
474 openSetNodes[currentKey] = nil
475
476 if currentKey == endKey then
477 -- Reconstruct path
478 local path = {}
479 while currentKey do
480 table.insert(path, 1, current)
481 currentKey = cameFrom[currentKey]
482 if currentKey then
483 local coords = {}
484 for coord in string.gmatch(currentKey, "([^,]+)") do
485 table.insert(coords, tonumber(coord))
486 end
487 current = { x = coords[1], y = coords[2], z = coords[3] }
488 else
489 current = nil
490 end
491 end
492 return path
493 end
494
495 -- Explore neighbors (allow passing through explored areas)
496 local neighbors = {
497 { x = current.x + 1, y = current.y, z = current.z },
498 { x = current.x - 1, y = current.y, z = current.z },
499 { x = current.x, y = current.y, z = current.z + 1 },
500 { x = current.x, y = current.y, z = current.z - 1 },
501 }
502
503 for _, neighbor in ipairs(neighbors) do
504 local neighborKey = posKey(neighbor)
505 local tentative_gScore = gScore[currentKey] + 1
506
507 -- Update the logic to allow traversal through explored areas to reach unknown areas
508 local cellStatus = getCellStatus(neighbor.x, neighbor.y, neighbor.z)
509 local passable = (cellStatus ~= "wall") -- Only walls are impassable
510
511 if passable then
512 if gScore[neighborKey] == nil or tentative_gScore < gScore[neighborKey] then
513 cameFrom[neighborKey] = currentKey
514 gScore[neighborKey] = tentative_gScore
515 fScore[neighborKey] = tentative_gScore + heuristic(neighbor, endPos)
516 if not openSetNodes[neighborKey] then
517 openSet:push(neighbor, fScore[neighborKey])
518 openSetNodes[neighborKey] = true
519 end
520 end
521 end
522 end
523 end
524
525 return nil -- Path not found
526end
527
528-- Exploration Functions
529local function explore()
530 log("Starting exploration...")
531 local unexplored = {}
532 local visited = {}
533
534 local startingPosition = { x = position.x, y = position.y, z = position.z }
535
536 table.insert(unexplored, { x = position.x, y = position.y, z = position.z })
537
538 while #unexplored > 0 do
539 local current = table.remove(unexplored, 1)
540 local currentKey = posKey(current)
541 if not visited[currentKey] then
542 visited[currentKey] = true
543
544 -- Check boundaries
545 if math.abs(current.x - startingPosition.x) <= areaSize and
546 math.abs(current.z - startingPosition.z) <= areaSize then
547
548 if navigateTo(current, true) then
549 detectAndUpdate()
550 -- Check adjacent cells, including above and below
551 local neighbors = {
552 { x = current.x + 1, y = current.y, z = current.z },
553 { x = current.x - 1, y = current.y, z = current.z },
554 { x = current.x, y = current.y, z = current.z + 1 },
555 { x = current.x, y = current.y, z = current.z - 1 },
556 }
557
558 for _, neighbor in ipairs(neighbors) do
559 local neighborKey = posKey(neighbor)
560 if not visited[neighborKey] and getCellStatus(neighbor.x, neighbor.y, neighbor.z) == "unknown" then
561 table.insert(unexplored, neighbor)
562 end
563 end
564 else
565 logError("Failed to navigate to (" .. current.x .. ", " .. current.y .. ", " .. current.z .. ")")
566 end
567 end
568 end
569 end
570 log("Exploration completed.")
571end
572
573-- Navigation Function with enhanced exploration logic
574function navigateTo(targetPos, retryCount)
575 if retryCount == nil or retryCount == false then
576 retryCount = 0
577 end
578
579 if retryCount > 3 then
580 logError("Failed to navigate to (" .. targetPos.x .. ", " .. targetPos.y .. ", " .. targetPos.z .. ") after multiple attempts")
581 return false
582 end
583
584 if position.x == targetPos.x and position.y == targetPos.y and position.z == targetPos.z then
585 return true
586 end
587
588 local path = findPath({ x = position.x, y = position.y, z = position.z }, targetPos)
589 if not path then
590 logError("No path found to (" .. targetPos.x .. ", " .. targetPos.y .. ", " .. targetPos.z .. "). Retrying...")
591 return navigateTo(targetPos, retryCount + 1)
592 end
593
594 -- Follow the path
595 for i = 2, #path do
596 local current = path[i - 1]
597 local next = path[i]
598 local dx = next.x - current.x
599 local dz = next.z - current.z
600
601 local moveSuccess = false
602 if dx == 1 then
603 faceDirection(0)
604 elseif dx == -1 then
605 faceDirection(2)
606 elseif dz == 1 then
607 faceDirection(1)
608 elseif dz == -1 then
609 faceDirection(3)
610 end
611 moveSuccess = moveForward()
612
613 if not moveSuccess then
614 logError("Failed to move at (" .. position.x .. ", " .. position.y .. ", " .. position.z .. "). Retrying...")
615 detectAndUpdate()
616 saveMapData()
617 return navigateTo(targetPos, retryCount + 1)
618 end
619
620 position = next
621 detectAndUpdate()
622 end
623 return true
624end
625
626-- Inventory Management Functions
627local function hasEmptySlot()
628 for slot = 1, turtleInventorySize do
629 if turtle.getItemCount(slot) == 0 then
630 return true
631 end
632 end
633 return false
634end
635
636local function selectEmptySlot()
637 for slot = 1, turtleInventorySize do
638 if turtle.getItemCount(slot) == 0 then
639 turtle.select(slot)
640 return true
641 end
642 end
643 return false
644end
645
646local function selectItemSlot(itemName)
647 for slot = 1, turtleInventorySize do
648 local item = turtle.getItemDetail(slot)
649 if item and item.name == itemName then
650 turtle.select(slot)
651 return true
652 end
653 end
654 return false
655end
656
657-- Chest Interaction Functions
658local function scanForChest()
659 local success, data = turtle.inspect()
660 if success and data.name == "minecraft:chest" then
661 return true
662 end
663 return false
664end
665
666local function getChestContents()
667 local chest = peripheral.wrap("front")
668 if chest then
669 return chest.list()
670 else
671 logError("No chest detected at (" .. position.x .. ", " .. position.y .. ", " .. position.z .. ")")
672 return nil
673 end
674end
675
676-- Process Chests
677function processChests()
678 log("Processing chests...")
679 local processedCount = 0
680 local errorCount = 0
681
682 for key, chestInfo in pairs(chestData) do
683 local chestPos = chestInfo.position
684 if navigateTo(chestPos, false) then
685 faceDirection(chestInfo.direction) -- Ensure facing chest
686 local chest = peripheral.wrap("front")
687 if chest then
688 local items = chest.list()
689 for slot, item in pairs(items) do
690 local itemCategory = getItemCategory(item.name)
691 if itemCategory ~= chestInfo.category then
692 if not selectEmptySlot() then
693 logError("Turtle inventory full. Returning to base to unload.")
694 return false
695 end
696 if chest.pullItems(peripheral.getName("turtle"), slot) then
697 log("Collected " .. item.count .. "x " .. item.name .. " from chest at (" .. position.x .. ", " .. position.y .. ", " .. position.z .. ")")
698 local targetChestInfo = findChestForItem(item.name)
699 if targetChestInfo then
700 if navigateTo(targetChestInfo.position, false) then
701 faceDirection(targetChestInfo.direction)
702 local targetChest = peripheral.wrap("front")
703 if targetChest then
704 if selectItemSlot(item.name) then
705 if turtle.drop() then
706 log("Moved " .. item.count .. "x " .. item.name .. " to chest at (" .. position.x .. ", " .. position.y .. ", " .. position.z .. ")")
707 processedCount = processedCount + 1
708 else
709 logError("Failed to deposit item into chest.")
710 errorCount = errorCount + 1
711 end
712 else
713 logError("Item not found in turtle inventory.")
714 errorCount = errorCount + 1
715 end
716 else
717 logError("Failed to access target chest.")
718 errorCount = errorCount + 1
719 end
720 else
721 logError("Failed to navigate to target chest.")
722 errorCount = errorCount + 1
723 end
724 else
725 logError("No appropriate chest found for item.")
726 errorCount = errorCount + 1
727 end
728 else
729 logError("Failed to collect item from chest.")
730 errorCount = errorCount + 1
731 end
732 end
733 end
734 else
735 logError("Failed to access chest.")
736 errorCount = errorCount + 1
737 end
738 else
739 logError("Failed to navigate to chest.")
740 errorCount = errorCount + 1
741 end
742 end
743 log("Chest processing completed. Processed items: " .. processedCount .. ", Errors: " .. errorCount)
744 return true
745end
746
747-- Find Appropriate Chest for an Item
748function findChestForItem(itemName)
749 local itemCategory = getItemCategory(itemName)
750 for _, chestInfo in pairs(chestData) do
751 if chestInfo.category == itemCategory then
752 return chestInfo
753 end
754 end
755 for _, chestInfo in pairs(chestData) do
756 if chestInfo.category == "miscellaneous" then
757 return chestInfo
758 end
759 end
760 for _, chestInfo in pairs(chestData) do
761 if chestInfo.category == "junk" then
762 return chestInfo
763 end
764 end
765 return nil
766end
767
768-- Main Program
769local function main()
770 getUserInput()
771 initializeItemCategories()
772
773 if loadExistingMapData then
774 loadMapData()
775 else
776 map = {}
777 end
778
779 if loadExistingChestData then
780 loadChestData()
781 else
782 chestData = {}
783 end
784
785 if mappingEnabled then
786 explore()
787 for x, xData in pairs(map) do
788 for y, yData in pairs(xData) do
789 for z, status in pairs(yData) do
790 if status == "chest" then
791 local chestKey = x .. "," .. y .. "," .. z
792 if not chestData[chestKey] then
793 local chestPos = { x = x, y = y, z = z }
794 if navigateTo(chestPos, false) then
795 local chestDirection = direction
796 faceDirection(chestDirection)
797 local items = getChestContents()
798 if items then
799 local chestCategory = determineChestCategory(items)
800 chestData[chestKey] = {
801 position = chestPos,
802 category = chestCategory,
803 direction = chestDirection
804 }
805 log("Chest at (" .. x .. ", " .. y .. ", " .. z .. ") categorized as " .. chestCategory)
806 end
807 else
808 logError("Failed to navigate to chest.")
809 end
810 end
811 end
812 end
813 end
814 end
815 saveChestData()
816 end
817
818 local explorationTimer = os.startTimer(600) -- Re-explore every 10 minutes
819 local running = true
820
821 local function safeExplore()
822 local success, errorMsg = pcall(explore)
823 if not success then
824 logError("Exploration failed: " .. errorMsg)
825 print("Exploration encountered an error. Check the log file for details.")
826 end
827 end
828
829 while running do
830 local event, param = os.pullEvent()
831
832 if event == "timer" and param == explorationTimer then
833 log("Starting scheduled exploration...")
834 safeExplore()
835 saveMapData()
836 saveChestData()
837 explorationTimer = os.startTimer(600)
838 log("Scheduled exploration completed.")
839 elseif event == "terminate" then
840 print("Program termination requested. Cleaning up...")
841 saveMapData()
842 saveChestData()
843 log("Program terminated by user.")
844 running = false
845 end
846
847 if turtle.getFuelLevel() < fuelThreshold then
848 if not refuel() then
849 logError("Critical fuel level. Unable to refuel. Stopping program.")
850 running = false
851 end
852 end
853 end
854
855 print("Program terminated. Check logs for details.")
856end
857
858-- Run the Program
859local success, errorMessage = pcall(main)
860if not success then
861 logError("Program terminated unexpectedly: " .. errorMessage)
862 print("An error occurred. Check the log file for details.")
863end
864