room_carver.lua
355 lines · 14.6 KB
Room Carver (lean) Start position: place turtle at the room's bottom-left corner, on floor level, facing into the room. Goal: carve a rectangular room using efficient reach (minimal vertical moves), place torches, and auto-trash matching items when full. ]]-- local version = "2.2" -- UI helpers (minimal) local function ask_number_default(prompt, default_value, min_val, max_val) while true do term.clear(); term.setCursorPos(1,1) write("Room Carver v"..version.."\n\n") write(string.format("%s [default: %s] ", prompt, tostring(default_value))) local s = read(); if s == nil or s == "" then return default_value end local n = tonumber(s) if n and (not min_val or n >= min_val) and (not max_val or n <= max_val) then return n end write("\nInvalid input. Press Enter to try again."); read() end end local function ask_yes_no(prompt, default_yes) while true do term.clear(); term.setCursorPos(1,1) write("Room Carver v"..version.."\n\n") local def = default_yes and "Y" or "N" write(string.format("%s (y/n) [default: %s] ", prompt, def)) local s = read(); s = s and string.lower(s) or "" if s == "" then return default_yes end if s == "y" or s == "yes" then return true end if s == "n" or s == "no" then return false end
Copy & run
wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/room_carver.lua
| 1 | --[[ |
| 2 | Room Carver (lean) |
| 3 | Start position: place turtle at the room's bottom-left corner, on floor level, facing into the room. |
| 4 | Goal: carve a rectangular room using efficient reach (minimal vertical moves), place torches, and auto-trash matching items when full. |
| 5 | ]]-- |
| 6 | |
| 7 | local version = "2.2" |
| 8 | |
| 9 | -- UI helpers (minimal) |
| 10 | local function ask_number_default(prompt, default_value, min_val, max_val) |
| 11 | while true do |
| 12 | term.clear(); term.setCursorPos(1,1) |
| 13 | write("Room Carver v"..version.."\n\n") |
| 14 | write(string.format("%s [default: %s] ", prompt, tostring(default_value))) |
| 15 | local s = read(); if s == nil or s == "" then return default_value end |
| 16 | local n = tonumber(s) |
| 17 | if n and (not min_val or n >= min_val) and (not max_val or n <= max_val) then return n end |
| 18 | write("\nInvalid input. Press Enter to try again."); read() |
| 19 | end |
| 20 | end |
| 21 | local function ask_yes_no(prompt, default_yes) |
| 22 | while true do |
| 23 | term.clear(); term.setCursorPos(1,1) |
| 24 | write("Room Carver v"..version.."\n\n") |
| 25 | local def = default_yes and "Y" or "N" |
| 26 | write(string.format("%s (y/n) [default: %s] ", prompt, def)) |
| 27 | local s = read(); s = s and string.lower(s) or "" |
| 28 | if s == "" then return default_yes end |
| 29 | if s == "y" or s == "yes" then return true end |
| 30 | if s == "n" or s == "no" then return false end |
| 31 | write("\nInvalid input. Press Enter to try again."); read() |
| 32 | end |
| 33 | end |
| 34 | |
| 35 | -- Movement/helpers |
| 36 | local current_fuel_slot = 2 |
| 37 | local heading_index = 0 -- 0:+Z, 1:+X, 2:-Z, 3:-X |
| 38 | local function turn_left() turtle.turnLeft(); heading_index = (heading_index + 3) % 4 end |
| 39 | local function turn_right() turtle.turnRight(); heading_index = (heading_index + 1) % 4 end |
| 40 | local function face_dir(target) |
| 41 | local diff = (target - heading_index) % 4 |
| 42 | if diff == 1 then turn_right() |
| 43 | elseif diff == 2 then turn_right(); turn_right() |
| 44 | elseif diff == 3 then turn_left() end |
| 45 | end |
| 46 | local function ensure_fuel(minimum) |
| 47 | if turtle.getFuelLevel()=="unlimited" then return end |
| 48 | if turtle.getFuelLevel()>=minimum then return end |
| 49 | -- Prefer configured fuel slot first |
| 50 | if current_fuel_slot and current_fuel_slot>=1 and current_fuel_slot<=16 then |
| 51 | turtle.select(current_fuel_slot) |
| 52 | if turtle.refuel(0) then while turtle.getFuelLevel()<minimum and turtle.refuel(1) do end end |
| 53 | if turtle.getFuelLevel()>=minimum then return end |
| 54 | end |
| 55 | -- Fallback: try all slots |
| 56 | for i=1,16 do turtle.select(i); if turtle.refuel(0) then while turtle.getFuelLevel()<minimum and turtle.refuel(1) do end; if turtle.getFuelLevel()>=minimum then return end end end |
| 57 | term.clear(); term.setCursorPos(1,1); print("Out of fuel. Add fuel and press Enter."); read(); return ensure_fuel(minimum) |
| 58 | end |
| 59 | local function dig_forward() while turtle.detect() do if not turtle.dig() then turtle.attack(); sleep(0.05) end end end |
| 60 | local function dig_up() while turtle.detectUp() do if not turtle.digUp() then turtle.attackUp(); sleep(0.05) end end end |
| 61 | local function dig_down() while turtle.detectDown() do if not turtle.digDown() then turtle.attackDown(); sleep(0.05) end end end |
| 62 | local function safe_forward() ensure_fuel(100); while not turtle.forward() do dig_forward(); sleep(0.02) end end |
| 63 | local function safe_up() ensure_fuel(100); while not turtle.up() do dig_up(); sleep(0.02) end end |
| 64 | local function safe_down() ensure_fuel(100); while not turtle.down() do dig_down(); sleep(0.02) end end |
| 65 | local function step_right() turn_right(); dig_forward(); safe_forward(); turn_left() end |
| 66 | local function step_left() turn_left(); dig_forward(); safe_forward(); turn_right() end |
| 67 | |
| 68 | -- Inventory helpers |
| 69 | local function empty_slots() |
| 70 | local e=0; for i=1,16 do if turtle.getItemCount(i)==0 then e=e+1 end end; return e |
| 71 | end |
| 72 | local function any_match(sample_slot) |
| 73 | if not sample_slot or turtle.getItemCount(sample_slot)==0 then return false end |
| 74 | for i=1,16 do if i~=sample_slot and turtle.getItemCount(i)>0 then turtle.select(i); if turtle.compareTo(sample_slot) then return true end end end |
| 75 | return false |
| 76 | end |
| 77 | local function drop_matching_front(sample_slot) |
| 78 | if not sample_slot or turtle.getItemCount(sample_slot)==0 then return end |
| 79 | for i=1,16 do |
| 80 | if i~=sample_slot and turtle.getItemCount(i)>0 then |
| 81 | turtle.select(i) |
| 82 | if turtle.compareTo(sample_slot) then dig_forward(); turtle.drop() end |
| 83 | end |
| 84 | end |
| 85 | end |
| 86 | |
| 87 | -- Persistent resume state |
| 88 | local function state_path() |
| 89 | local dir = ".room_carver"; if not fs.exists(dir) then fs.makeDir(dir) end; return fs.combine(dir, "state") |
| 90 | end |
| 91 | local function save_state(st) |
| 92 | st.heading_index = heading_index |
| 93 | local p = state_path() |
| 94 | local h = fs.open(p, "w"); if not h then return end |
| 95 | h.write(textutils.serialize(st)) |
| 96 | h.close() |
| 97 | end |
| 98 | local function load_state() |
| 99 | local p = state_path(); if not fs.exists(p) then return nil end |
| 100 | local h = fs.open(p, "r"); if not h then return nil end |
| 101 | local d = h.readAll(); h.close() |
| 102 | local ok, t = pcall(textutils.unserialize, d) |
| 103 | if ok and type(t)=="table" then return t end |
| 104 | return nil |
| 105 | end |
| 106 | local function clear_state() |
| 107 | local p = state_path(); if fs.exists(p) then fs.delete(p) end |
| 108 | end |
| 109 | |
| 110 | -- Torch placement on side wall we are adjacent to (left if at x==1, right if at x==width) |
| 111 | local function place_torch_on_wall(torch_slot, on_left) |
| 112 | if torch_slot<=0 or turtle.getItemCount(torch_slot)==0 then return end |
| 113 | turtle.select(torch_slot) |
| 114 | if on_left then turn_left() else turn_right() end |
| 115 | local ok=turtle.place() |
| 116 | if not ok then dig_forward(); ok=turtle.place() end |
| 117 | if on_left then turn_right() else turn_left() end |
| 118 | end |
| 119 | |
| 120 | -- Traverse helpers |
| 121 | local function lawn_step_side(to_right) |
| 122 | if to_right then step_right() else step_left() end |
| 123 | end |
| 124 | local function advance_row() |
| 125 | -- Ensure we move along current forward Z direction |
| 126 | dig_forward(); safe_forward() |
| 127 | end |
| 128 | |
| 129 | -- Inventory + IO helpers for capacity and chest dump |
| 130 | local function ensure_capacity(sample_slot) |
| 131 | if sample_slot and empty_slots()==0 and any_match(sample_slot) then drop_matching_front(sample_slot) end |
| 132 | end |
| 133 | local function turn_to_side(side) if side=="left" then turn_left() else turn_right() end end |
| 134 | local function place_chest_in_wall(chest_slot, side) |
| 135 | if chest_slot<=0 or turtle.getItemCount(chest_slot)==0 then return false end |
| 136 | turn_to_side(side) |
| 137 | dig_forward() |
| 138 | turtle.select(chest_slot) |
| 139 | local ok=turtle.place() |
| 140 | if not ok then dig_forward(); ok=turtle.place() end |
| 141 | turn_to_side(side=="left" and "right" or "left") |
| 142 | return ok |
| 143 | end |
| 144 | local function deposit_into_front(reserved_slots) |
| 145 | for i=1,16 do |
| 146 | if not reserved_slots[i] and turtle.getItemCount(i)>0 then |
| 147 | turtle.select(i) |
| 148 | if not turtle.refuel(0) then turtle.drop() end |
| 149 | end |
| 150 | end |
| 151 | end |
| 152 | local function ensure_inventory_capacity(cfg, at_left) |
| 153 | if empty_slots()>0 then return end |
| 154 | -- First try throwing matches |
| 155 | if cfg.use_throw and any_match(cfg.throw_slot) then drop_matching_front(cfg.throw_slot); if empty_slots()>0 then return end end |
| 156 | -- Then try chests |
| 157 | if cfg.use_chests and turtle.getItemCount(cfg.chest_slot)>0 then |
| 158 | local side = at_left and "left" or "right" |
| 159 | if place_chest_in_wall(cfg.chest_slot, side) then |
| 160 | -- Face chest now; deposit everything except reserved slots |
| 161 | turn_to_side(side) |
| 162 | local reserved = {} |
| 163 | reserved[cfg.fuel_slot]=true; reserved[cfg.chest_slot]=true; reserved[cfg.torch_slot]=true; reserved[cfg.throw_slot]=true |
| 164 | deposit_into_front(reserved) |
| 165 | -- Keep exactly 1 item in throw sample slot if present |
| 166 | if cfg.use_throw and turtle.getItemCount(cfg.throw_slot)>1 then |
| 167 | turtle.select(cfg.throw_slot); turtle.drop(turtle.getItemCount(cfg.throw_slot)-1) |
| 168 | end |
| 169 | turn_to_side(side=="left" and "right" or "left") |
| 170 | end |
| 171 | end |
| 172 | end |
| 173 | |
| 174 | -- Optimized row drilling: turn once per row, traverse along X, turn back to +Z |
| 175 | local function advance_row_z(z_forward) |
| 176 | face_dir(z_forward and 0 or 2) |
| 177 | dig_forward(); safe_forward() |
| 178 | end |
| 179 | |
| 180 | -- Returns whether we ended on the right edge |
| 181 | local function line_snake_traverse(width, depth, per_cell_fn, on_row_start, start_from_right, z_forward) |
| 182 | local going_right = not start_from_right |
| 183 | for zi=1,depth do |
| 184 | local row_from_front = z_forward and zi or (depth - zi + 1) |
| 185 | if on_row_start then on_row_start(row_from_front, going_right) end |
| 186 | -- Face along X for this row |
| 187 | face_dir(going_right and 1 or 3) -- 1:+X, 3:-X |
| 188 | for x=1,width do |
| 189 | per_cell_fn(x, row_from_front, going_right) |
| 190 | if x<width then dig_forward(); safe_forward() end |
| 191 | end |
| 192 | -- Advance to next row along Z |
| 193 | if zi<depth then advance_row_z(z_forward) end |
| 194 | going_right = not going_right |
| 195 | end |
| 196 | -- Determine ending edge: after depth rows, if depth is odd we end opposite of start edge |
| 197 | local ended_right = (depth % 2 == 0) and start_from_right or (not start_from_right) |
| 198 | return ended_right |
| 199 | end |
| 200 | |
| 201 | -- Multi-band carve to support arbitrary height |
| 202 | local function carve_room(width, depth, height, cfg) |
| 203 | if height < 2 then height = 2 end |
| 204 | local bands |
| 205 | if height == 2 then bands = 1 else bands = math.ceil((height-1)/2) end |
| 206 | -- Move to working level for first band |
| 207 | if height >= 3 then safe_up() end |
| 208 | local start_from_right = cfg.resume_start_from_right or false -- band 1 starts at left edge by default |
| 209 | local z_forward = (cfg.resume_z_forward==false) and false or true -- default forward |
| 210 | local start_band = cfg.resume_band or 1 |
| 211 | local start_row = cfg.resume_row or 1 |
| 212 | for b=start_band,bands do |
| 213 | local skip_up = (height >= 3) and (b == bands) and ((2*b + 1) > height) |
| 214 | local function per_cell_fn(x, z, going_right) |
| 215 | -- Keep inventory flowing |
| 216 | ensure_inventory_capacity(cfg, going_right) |
| 217 | -- Clear vertical for this cell using reach |
| 218 | if height == 2 then |
| 219 | dig_up() |
| 220 | else |
| 221 | dig_down(); if not skip_up then dig_up() end |
| 222 | end |
| 223 | end |
| 224 | local function on_row_start(z, going_right) |
| 225 | -- Capacity check at row start |
| 226 | ensure_inventory_capacity(cfg, going_right) |
| 227 | -- Torches only on first band |
| 228 | if b==1 and cfg.use_torches and cfg.torch_spacing>0 and (z % cfg.torch_spacing == 0) then |
| 229 | place_torch_on_wall(cfg.torch_slot, going_right) |
| 230 | end |
| 231 | -- Fuel guard: small rolling reserve based on remaining rows and columns |
| 232 | local remaining_rows = z_forward and (depth - z + 1) or (z) |
| 233 | local reserve = math.max(40, remaining_rows + width + 10) |
| 234 | ensure_fuel(reserve) |
| 235 | -- Persist state at row boundary |
| 236 | save_state({width=width, depth=depth, height=height, band=b, row=z, start_from_right=start_from_right, z_forward=z_forward}) |
| 237 | end |
| 238 | -- Traverse this band from the correct corner and along proper Z direction |
| 239 | local depth_remaining = (b==start_band) and (depth - (start_row-1)) or depth |
| 240 | local ended_right = line_snake_traverse(width, depth_remaining, per_cell_fn, on_row_start, start_from_right, z_forward) |
| 241 | -- Move up to next band |
| 242 | if b < bands then safe_up(); safe_up() end |
| 243 | -- Prepare next band start: flip Z direction and set start edge to where we ended |
| 244 | start_from_right = ended_right |
| 245 | z_forward = not z_forward |
| 246 | start_row = 1 |
| 247 | end |
| 248 | -- Clear state when finished |
| 249 | clear_state() |
| 250 | end |
| 251 | |
| 252 | local function main() |
| 253 | term.clear(); term.setCursorPos(1,1) |
| 254 | print("Room Carver v"..version) |
| 255 | print("Start at bottom-left, on floor, facing into room.") |
| 256 | |
| 257 | -- Favorites |
| 258 | local function favorite_path() local dir = ".room_carver"; if not fs.exists(dir) then fs.makeDir(dir) end; return fs.combine(dir, "favorite") end |
| 259 | local function load_favorite() |
| 260 | local p=favorite_path(); if not fs.exists(p) then return nil end |
| 261 | local h=fs.open(p,"r"); if not h then return nil end |
| 262 | local d=h.readAll(); h.close(); local ok,t=pcall(textutils.unserialize,d); if ok and type(t)=="table" then return t end; return nil |
| 263 | end |
| 264 | local function save_favorite(tbl) |
| 265 | local p=favorite_path(); local h=fs.open(p,"w"); if not h then return end; h.write(textutils.serialize(tbl)); h.close() |
| 266 | end |
| 267 | |
| 268 | local fav = load_favorite() |
| 269 | local use_fav = fav and ask_yes_no("Use favorite saved config?", true) or false |
| 270 | |
| 271 | local width, depth, height |
| 272 | local use_torches, torch_spacing, torch_slot |
| 273 | local use_throw, throw_slot |
| 274 | local use_chests, chest_slot |
| 275 | local fuel_slot |
| 276 | |
| 277 | if use_fav then |
| 278 | width=fav.width or 9; depth=fav.depth or 9; height=fav.height or 3 |
| 279 | use_torches = fav.use_torches~=false; torch_spacing=fav.torch_spacing or 9; torch_slot=fav.torch_slot or 1 |
| 280 | use_throw = fav.use_throw~=false; throw_slot=fav.throw_slot or 4 |
| 281 | use_chests = fav.use_chests~=false; chest_slot=fav.chest_slot or 3 |
| 282 | fuel_slot = fav.fuel_slot or 2 |
| 283 | else |
| 284 | width = ask_number_default("Room width (X):", 9, 1, 199) |
| 285 | depth = ask_number_default("Room depth (Z):", 9, 1, 199) |
| 286 | height = ask_number_default("Room height (>=2):", 3, 2, 32) |
| 287 | use_torches = ask_yes_no("Place torches?", true) |
| 288 | if use_torches then |
| 289 | torch_spacing = ask_number_default("Torch spacing (rows):", 9, 1, 64) |
| 290 | torch_slot = ask_number_default("Torch slot:", 1, 1, 16) |
| 291 | else |
| 292 | torch_spacing = 0; torch_slot = 1 |
| 293 | end |
| 294 | use_throw = ask_yes_no("Auto-throw matching items when full?", true) |
| 295 | throw_slot = ask_number_default("Sample slot to match:", 4, 1, 16) |
| 296 | use_chests = ask_yes_no("Place chest to dump when full?", true) |
| 297 | chest_slot = ask_number_default("Chest slot:", 3, 1, 16) |
| 298 | fuel_slot = ask_number_default("Fuel slot:", 2, 1, 16) |
| 299 | if ask_yes_no("Save as favorite?", true) then |
| 300 | save_favorite({width=width,depth=depth,height=height,use_torches=use_torches,torch_spacing=torch_spacing,torch_slot=torch_slot,use_throw=use_throw,throw_slot=throw_slot,use_chests=use_chests,chest_slot=chest_slot,fuel_slot=fuel_slot}) |
| 301 | end |
| 302 | end |
| 303 | |
| 304 | current_fuel_slot = fuel_slot or 2 |
| 305 | |
| 306 | -- Attempt to resume from saved state if present; ask before using it |
| 307 | local st = load_state() |
| 308 | if st and st.width==width and st.depth==depth and st.height==height then |
| 309 | local prompt = string.format("Found saved progress for this size: band %d, row %d. Restore?", st.band or 1, st.row or 1) |
| 310 | local restore = ask_yes_no(prompt, true) |
| 311 | if not restore then |
| 312 | clear_state() |
| 313 | st = nil |
| 314 | end |
| 315 | end |
| 316 | |
| 317 | term.clear(); term.setCursorPos(1,1) |
| 318 | print("Room Carver v"..version) |
| 319 | print("Size:", width, "x", depth, " height:", height) |
| 320 | if use_torches then print("Torches every", torch_spacing, "rows from slot", torch_slot) else print("Torches: off") end |
| 321 | print("Fuel slot:", current_fuel_slot) |
| 322 | print("Chest dump:", use_chests and ("slot "..(chest_slot or 3)) or "off") |
| 323 | print("Auto-throw:", use_throw and ("sample slot "..(throw_slot or 4)) or "off") |
| 324 | print("Press Enter to start...") |
| 325 | read() |
| 326 | |
| 327 | -- Estimate minimal fuel and ensure (soft check; we now check per-row too) |
| 328 | local est_moves = (width*depth) * math.max(1, math.ceil((height-1)/2)) * 2 + depth + width |
| 329 | ensure_fuel(math.min(est_moves + 50, 500)) |
| 330 | |
| 331 | -- State (if any) was already handled above |
| 332 | |
| 333 | -- Carve with full configuration |
| 334 | carve_room(width, depth, height, { |
| 335 | use_torches=use_torches, |
| 336 | torch_spacing=torch_spacing or 0, |
| 337 | torch_slot=torch_slot or 1, |
| 338 | use_throw=use_throw, |
| 339 | throw_slot=throw_slot or 4, |
| 340 | use_chests=use_chests, |
| 341 | chest_slot=chest_slot or 3, |
| 342 | fuel_slot=current_fuel_slot, |
| 343 | -- resume hints |
| 344 | resume_band = st and st.band or 1, |
| 345 | resume_row = st and st.row or 1, |
| 346 | resume_start_from_right = st and st.start_from_right or false, |
| 347 | resume_z_forward = st and (st.z_forward~=false) |
| 348 | }) |
| 349 | |
| 350 | term.clear(); term.setCursorPos(1,1) |
| 351 | print("Done. Room completed.") |
| 352 | end |
| 353 | |
| 354 | main() |
| 355 |