Mods/basic robot/Programs

From Minetest


Programs for support/maintainance/admins.
They might need admin- or puzzle-privileges.

Administration

Mailbox

Shows a form with a field for textinput and a "SEND"-button to the player,
stores the text in a book.
For mailing feedback to the admin.

-- feedback-form, based on rnd's mailrobot
if not pn then pn="Mailbox"  -- hajo 2017-03-07
  s=0
  daynr=os.date("%w")  -- %w : weekday [0-6 = Sunday-Saturday]
  booknr=tonumber(daynr);  -- say(daynr.."/"..booknr)
  msgsize = 2500;
  _,text = book.read(booknr); text = text or "";
  
	write_msg = function(sender,msg)
		local newsize = string.len(text)+string.len(msg);
		if newsize>msgsize then return "messages space exceeded" end
		text = text .. "\n"..os.date("%Y-%m-%d %H:%M") .. " " .. sender .. ": " .. msg; 
		book.write(booknr,"messages #"..booknr, text)
	end

end

--textarea[X,Y;W,H;name;label;default]
--button[X,Y;W,H;name;label]
if s == 0 then
	players = find_player(4);
	if players and players[1] then
		s=1
		local form = "size[8,4.5]" ..
		"textarea[0,0;9,4.5;msg;YOUR FEEDBACK-MESSAGE;]"..
		"button_exit[-0.5,4.15;2,1;send;send]"
		self.show_form(players[1],form)
	end
elseif s==1 then
	sender,fields = self.read_form();
	if sender then
		if fields.send then
			msg = fields.msg;
			if msg and msg~="" then
				write_msg(sender,msg); -- activate.up(1)
				--_G.minetest.chat_send_player(sender,"#mailbot: your message has been stored")
				say("#mailbot: your message has been stored")
			end
		end
		self.remove()
	end
end
--

This variant uses separate books for each day of the week.
Without access to _G, the program can also be used by regular users as their mailbox.

To read the messages, open the books in the robot's library.

Poll/Vote

Shows a form with text and several buttons to choose from to the player,
stores which button was choosen in a book.
This is much like the mailbox above, with buttons instead of a field for text-input.

--todo

Spawn-Quiz

This is the spawn-quiz from the old robots-server.

On the old robot-server, the spawn-building was like a cage,
and to get out, players had to solve this quiz.

The program shows a dialog with a math-quiz, like "2 * 3 + 4 = ?",
with 5 multiple-choice-buttons in a formspec.

When solved, the player is teleported out of the spawn-building.

    -- rnd - Spawn-Quiz @ -7 -3 5, with dialog/formspec.
    -- When solved, gives apple, and teleports the player out of the spawn-building.
 
    if not s then
       s=0
       t=0
       option = {"A","B","C","D","E"}
       generate_question = function()
          local a = math.random(10)+0;
          local b = math.random(10)+0;
          local c = math.random(20)-10;
          local d = a*b+c;
          msg = "To get out solve the math problem\n\n";
          msg = msg .. a.."*"..b.."+"..c .. " = ?\n\n"
          problem = a.."*"..b.."+"..c .. " = ?";
          correct = math.random(5);
          local frm = "";
 
          for i =1,5 do
             local offset = 0;
             if i~=correct then offset = math.random(10)-5; if offset == 0 then offset = -1 end end
             frm = frm .. "button_exit[".. -0.25+(i-1)*1.25 ..",1.75;1.25,1;" .. i .. ";".. d + offset .. "]"
          end
       
          local form = "size[6,2.25]" .. "textarea[0.05,-0.3;6.5,2.25;message;;".. msg.."]"..frm;
          return form, correct
       end
 
       selection = 1;
       question  = "";
       problem   = "";
    end
 
    if t%10 == 0 then  -- new quiz every 10s
       t = 0; form,selection = generate_question();
       local players = find_player(4);
       if players then
          for _,pname in ipairs(players) do
             self.show_form(pname,form)
          end
       end
    end
    t=t+1;
 
    sender,fields = self.read_form()
    if sender then
       player = _G.minetest.get_player_by_name(sender);
       if player then
          
          answer = 0;
          for i = 1,5 do if fields[_G.tostring(i)] then answer = i end end
          
          if answer == correct then
             player:setpos({x=-4,y=2,z=0})
             inv = player:get_inventory(); inv:add_item("main", "default:apple")
             _G.minetest.chat_send_player(sender,"<MATH ROBOT> congratulations, here is an apple.")
          elseif answer ~= 0 then
             player:setpos({x=0,y=-6,z=-1})
             say(sender .. " failed to solve the problem " .. problem)
          end
       end
    end

Commands like player:setpos(), player:get_inventory() and access to _G require admin-privileges.

Education

Math-Quiz

See Math_quiz1 and Math_quiz2.

They give rewards for solving questions like "WHAT IS 12*13 ?", via chat.

General-Quiz

From the forum, by rnd.
Show a quiz with text-questions, and buttons with multiple-choice answers.
The order of the answer-buttons is randomized on each run.

if not quiz then
-- QUIZ QUESTIONS:
-- FORMAT:  question,   answer1, answer2,...,   index of correct answer
 quiz = {
   {
      "QUESTION 1: what is the correct answer ?",
      "correct is a.",
      "correct is b.",
      "correct is c.",
      3 -- correct is 3rd answer - c.
   },

   {
      "QUESTION 2: what is the correct answer ?",
      "correct is a.",
      "correct is b.",
      "correct is c.",
      1 -- correct is a.
   },

   {
      "QUESTION 3: what is the correct answer ?",
      "correct is a.",
      "correct is b.",
      "correct is c.",
      2
   }

 }
 
 local permute = function(n,seed) -- return permuted array {1,..,n}
   _G.math.randomseed(seed);
   local permute = {}
   for i = 1, n do permute[i] = i end --input:sub(i, i)
   for i = n,2,-1 do
      local j = math.random(i-1);
      local tmp = permute[j];
      permute[j] = permute[i]; permute[i] = tmp;
   end
   return permute
 end

 get_form = function(k)
   
   local n = #quiz[k]-2;
   if n < 1 then error("quiz: error in question " .. k) end
   
   local frm = "label[0,0;" .. quiz[k][1] .. "] "
   order = permute(n,os.time())
   
   for i = 1,n do
      frm = frm .. "button_exit[0,".. (i)*1 ..";8,1;" .. order[i] .. ";".. quiz[k][1+order[i]].. "] "
   end
   
   local form = "size[8," .. (n+1) .. "]" .. frm;
   return form, quiz[k][#quiz[k]]
 end
 
 points   = 0; -- how many correct answers so far
 question = 1; -- current question
 correct  = 1; -- current correct answer
 state    = 1; -- 1: display question, 2: wait for answer

 local players = find_player(4);
 if not players then say("quiz: no players") self.remove() end
 pname = players[1];

end
 
if state == 1 then
   if question>#quiz then
      say("Congratulations! You answered all question. Your result: " .. points .. "/" .. #quiz)
      self.remove();
   else
      local form;
      form, correct = get_form(question)
      self.show_form(pname,form)
      state = 2 -- wait for answer
   end
elseif state == 2 then
   
   sender,fields = self.read_form()
   if sender then -- form event
      local pl = _G.minetest.get_player_by_name(pname);
      if pl then
         if fields.quit then -- button was pressed
            local selected = 0;
            -- what is the answer?
            for k,_ in pairs(fields) do  if k~="quit" then selected = tonumber(k) break end  end
            if selected>0 then
               if selected == correct then
                  say("correct!")
                  points=points+1
                  --correct: do something with player
               else
                  say("incorrect!")
                  -- incorrect: do something else with player
               end
               state = 1
               question = question + 1
            else
               say("quiz aborted")
               self.remove()
               -- no question was selected in form?
            end
         end
      end
   end

end

Service

Char-table

Make a table of all the keyboard-blocks, above the robot.

-- Char-table, by Kurik
local pos = self.pos()
pos.y = pos.y + 5
local p = {x=0,y=pos.y,z=0}
local i = 1

for z = -8,8 --[[ do ]] do
  for x = -8,7 --[[ do ]] do
    p.x = pos.x + x
    p.z = pos.z + z
    --local n = puzzle.get_node(p)
    --if n.name == 'air' then puzzle.set_node(p,node) end
  --i=0  -- air, for removing the table
    keyboard.set(p,i)
    i = i + 1
  end
end

self.remove()

Char-table2

A variant of the above, can limit the output to selected ranges, and set markers.

-- Char-table - 2017-10-14+

local pos = self.pos()
pos.y = pos.y + 5
local p = {x=0,y=pos.y,z=0}
local i = 1

for z = -8,8 --[[ do ]] do
  for x = -8,7 --[[ do ]] do
    p.x = pos.x + x
    p.z = pos.z + z
    keyboard.set(p,0) -- air

    r=0
    if i<=  6            then r=1 end -- colors 
    if i>=  7 and i<= 16 then r=2 end -- white numbers
    if i>= 65 and i<= 74 then r=3 end -- black numbers
    if i>= 82 and i<=107 then r=4 end -- uppercase chars
    if i>=114 and i<=139 then r=5 end -- lowercase chars

    if i==  17 then r=-1 end  -- set marker
    if i== 272 then r=-2 end 

    if r>0 then keyboard.set(p,i) end  -- !! adjust condition here !!
    if r==-1 then keyboard.set(p,4) end  -- green
    if r==-2 then keyboard.set(p,3) end  -- red marker

    i = i + 1
  end
  --say(i)
end

self.remove()

For example, change "if r>0" to "if r==1" to only show the colored keyboard-blocks.

Door-Opener

Place robot (with a keypad above it) next to a door,

Opens the locked doors inside a building owned by another player for guests,
eg. 'Hotels'.

-- simple door-opener - 2017-11
-- activated by keypad with password
 
if not i then i=0 end
 
i=i+1
n=read_node.forward();  --say(i..n)
n=string.sub(n,1,4)
if n=="door" then activate.forward(1); i=9 end
 
if i>=4 then self.remove() end 
turn.left()
--turn.right()

The robot turns around until it finds a door, then activates it = opens/closes it.

Teleporter

Move player to new location.

-- teleporter, by Kurik
puzzle.get_player('hajo'):move_to({x=100,y=9,z=100})
self.remove()

See also the mover from the mod basic_machines.

Shop

For selling stuff - see Simple_Shop.
It checks a chest above the robot for payment,
then exchanges the item(s) from the chest
for one (or more) item(s) from the robot's inventory.

As this uses item-names, it cannot sell unique stuff, eg. keys or books-with-text.

Shop2

A variant of the above shop.
This shop offers to sell several different items, with their own prices,
selected by pressing the different keypads/buttons that are placed near the robot.

if not pn then pn="Mike's Snack-Shop v0.6c";  -- 2017-03-29
self.spam(1)
pickup(3) --collect empty cups
t=""
steel1="default:steel_ingot"
gold1 ="default:gold_ingot"; gold2 ="default:gold_ingot 2"

n=read_node.down(); -- say(n)
if n=="basic_robot:spawner" then
  t=read_text.down()   -- text can be set with keypad 
end

slot=0
-- row 1,2,3,4 --> slot 1,9,17,25
if t=="snack1"  then slot= 1;  price=gold1 end -- donut
if t=="snack2"  then slot= 9;  price=gold2 end -- apple-donut
if t=="drink1"  then slot=17;  price=gold1 end -- cold coffee
if t=="drink2"  then slot=25;  price=gold2 end -- hot coffee
if slot<1 then say("config error:"..t) self.remove(); end

item = check_inventory.self("","main",slot)
if item=="" then 
  say(pn.."\n".."out of stock: slot"..slot); self.remove(); 
end
--say(slot.."->"..item.."!")

if not item then say("?") 
else 
  --say("<"..item..">") 
  cItem=1
  p=string.find(item," ")  
  if p then -- say("pos="..p)
    cItem = string.sub(item,p+1)  --!! tools also have a wear-count
  --say(p..":"..cItem)
    item1 = string.sub(item,1,p-1) 
 end

 say(item.."--> "..item1.." itemCount:"..cItem) --
  msg=pn.."\n".."selling "..item1.." for "..price
-- remove "default" and "farming"
  msg = string.gsub(msg, "default:", "") 
  msg = string.gsub(msg, "farming:", "")
  say(msg)
end

m=check_inventory.up(price,inv)
if m=="" then msg=msg.."\n no payment in chest !"
  say(msg); self.remove(); 
end

  ok2 = check_inventory.up( price,"main") -- look at chest above robot
  if not ok2 then
    price = string.gsub(price, "default:", "") 
    say("No payment in chest ("..price..")"); self.remove(); 
 end

--say("Selling "..m1.." for "..m2) 
  take.up(price)
  insert.up(item1)
  say("Thanks for your purchase !");
  take.up("farming:coffee_cup")  -- take back empty cups
  self.remove()
end
--

This shop sells snacks: cold and hot coffee, and two kinds of food.
Each row of the robot's inventory is used to store one of the different kinds of goods.
( but 1-2 spots somewhere need to be empty for the payments :)

By specifying the goods to sell by slotnumber, this robot can also sell unique items,
such as keys set to open specific doors, or books with different texts.

Setup:

Keypad1 _ Keypad3
Keypad2 Chest Keypad4
Sign _ Sign with prices
(Floor) Spawner (Floor)

Each keypad has to be set to point at the spawner, and write a unique text onto it, eg. "snack1".
The robot reads that text to determine which good to sell.

Admin-Shop

Remote-shop by rnd, from the server ROBOTS-SKYBLOCK.

It supplements the regular admin-shop of about 50 shops at spawn.
If the shop-bot is running, it can be contacted via chat-command "#shop".
It shows a list of items and prices, and directly accesses the players inventory
for payment and delivery.

if not s then
	s=0;item = 1; price =""; buyer = ""
	_G.minetest.forceload_block(self.pos(),true)
	_G.basic_robot.data[self.name()].obj:set_properties({nametag = ""})
	self.listen(1);self.spam(1)
	shoplist = {};
	--scan shops:
	pos = self.pos(); pos.y=pos.y-5; pos.x=pos.x-6
	pos1 = {x=pos.x-8,y=pos.y-2,z=pos.z-8};pos2 = {x=pos.x+8,y=pos.y+2,z=pos.z+8};
	local shoppos = _G.minetest.find_nodes_in_area(pos1, pos2, "shop:shop");
	--say("scanning... i found " .. #shoppos .. " shops ");
	count = 0
	for _,p in pairs(shoppos) do
		local inv = _G.minetest.get_meta(p):get_inventory()
		local s = inv:get_list("sell");local b = inv:get_list("buy")
		local k = s[1]:to_string();
		local v = b[1]:to_string();
		if (k and k~="") and (v and v~="") then count = count +1 shoplist[count] = {k,v}  end
	end
	
	local f_sort = function(a,b) return a[1]<b[1] end; 
	table.sort(shoplist,f_sort)
 
	itemlist = ""; count = 0;
	shopinventory = {};
	for k,v in pairs(shoplist) do
		if v[1] then
			count = count +1
			itemlist = itemlist .. string.format("%-5s%-40s%-32s",k,v[1],v[2]) .. ","
		end
	end
	t = 0; ct = 0;	ci = 0
	say("scanning shops ... completed. Added " .. count .. " items for sale ");
	
	player1="";player2 ="";
end
 
ct=ct+1
if ct%5 == 0 then
	ct=0
	ctext = "say #shop to buy";
	if ci>0 and ci<=count then
		if shoplist[ci] then
			iname = shoplist[ci][1];local p=string.find(iname,":");	if p then iname = string.sub(iname,p+1) end
			sname = shoplist[ci][2];p=string.find(sname,":");	if p then sname = string.sub(sname,p+1) end
			ctext = "#SHOP ".. ci .."\n" ..iname .. "\nPRICE\n" .. sname
		end
		ci=ci+1
	elseif ci == count+1 then ci=0
	elseif ci == 0 then ci=1
	end
	
	text = "SHOP ROBOT"..os.date("%x") .."  " .. os.date("%H:%M:%S").. "\n\n"..ctext
	self.display_text(text,10,3);
end
 
speaker,msg = self.listen_msg()
if msg then
	if s == 0 then
		if string.sub(msg,1,5)=="#shop" then
			if msg == "#shop" then
				--say("say #shop command, where command is list OR armor OR item number. Example: #shop list or #shop 1")
				
				local list = "1,2,3";
				local form = "size[8,8.5]" ..
				"label[0.,0.5;ID]"..
				"label[0.4,0.5;BUY]"..
				"label[3.,0.5;SELL]" ..
				"field[7.2,0.25;1.,1;count;count;".. 1 .."]"..
				"button[5.,-0.05;2.,1;ARMOR;ARMOR]"..
				"textlist[0,1;7.75,7.5;list;" .. itemlist .. "]";
				self.show_form(speaker,form)
			end
		elseif string.sub(msg,1,3) == "#tp" then
			local tname = string.sub(msg,5);
			player1 = speaker; player2 = tname; s = 1
			_G.minetest.chat_send_player(player2,"#SHOP: say tp to allow teleport of " .. player1)
		end
	elseif s == 1 then -- tp
		s=0; 
		if string.sub(msg,1,2)=="tp" and speaker == player2 then 
			local player = _G.minetest.get_player_by_name(player1);
			local tplayer = _G.minetest.get_player_by_name(player2);
			if player and tplayer then
				local p1 = player:getpos(); local p2 = tplayer:getpos();
				local price = math.floor(math.sqrt((p1.x-p2.x)^2+(p1.y-p2.y)^2+(p1.z-p2.z)^2)/100)+1;
				local inv = player:get_inventory(); 
				local stack = _G.ItemStack("default:gold_ingot " .. price);
				if price>0 and inv:contains_item("main",stack) then
					player:setpos(tplayer:getpos()); inv:remove_item("main",stack)
				elseif price>0 then
					_G.minetest.chat_send_player(player1,"You need " .. price .. " gold to teleport ")
				end
			end
		else 
			say("tp aborted.")
		end
		
	end
end
 
sender,fields = self.read_form()
if sender then
	local sel = fields.list; --"CHG:3"
	--say( string.gsub(_G.dump(fields),"\n",""))
	if sel and string.sub(sel,1,3) == "DCL" then
		local quantity = tonumber(fields.count) or 1;
		local select = tonumber(string.sub(sel,5) or "") or 1;
		local item, price
		
		if shoplist[select] then
			item,price = shoplist[select][1],shoplist[select][2];
		end
		
		local player = _G.minetest.get_player_by_name(sender);
		if player and item and price then
			
			local inv = player:get_inventory();
			if quantity > 99 then quantity = 99 end
			if quantity > 1 then
				local k = 1;
				local i = string.find(price," ");
				if i then 
					k = tonumber(string.sub(price,i+1)) or 1 
					price = string.sub(price,1,i-1).. " " .. k*quantity
				else 
					price = price.. " " .. quantity
				end
				
				k=1;i = string.find(item," ");
				if i then 
					k = tonumber(string.sub(item,i+1)) or 1 
					item = string.sub(item,1,i-1).. " " .. k*quantity
				else
					item = item .. " " .. quantity
				end
			end
			
			if inv:contains_item("main", price ) then 
				inv:remove_item("main",price)
				inv:add_item("main",item)
				_G.minetest.chat_send_player(sender,"#SHOP ROBOT: " .. item .. " sold to " .. sender .. " for " .. price)
			else 
				_G.minetest.chat_send_player(sender,"#SHOP ROBOT: you dont have " .. price .. " in inventory ")
			end
		end
	elseif fields.ARMOR then
		local player = _G.minetest.get_player_by_name(sender);
		if player then
			local inv = player:get_inventory();
			if inv:contains_item("main",_G.ItemStack("default:diamond 30")) then
				player:set_armor_groups({fleshy = 50})
				_G.minetest.chat_send_player(sender,"#SHOP ROBOT: you bought 50% damage reduction.")
			else
				_G.minetest.chat_send_player(sender,"#SHOP ROBOT: you need 30 diamonds to get armor effect")
			end
		end
	end
end

The robot sits under the roof above the admin-shop,
and scans the shops below to get the list of goods and prices.

The armor-item is special, the server has no mod for armor / 3d-armor.
But if a player has 30 diamonds, the shop can give him a 50% damage-reduction.


Replicator

Make copies of a block from the chest above the robot.

-- Replicator -  single, solid, placeable blocks only !     
-- No stone, desert-stone, basalt, slate, gneis, ORS. or cobble !

-- Also, no doors, or protectors ...
-- but trapdoors work

if not pn then pn="Replicator v1b";  --say(pn)  --hajo, 2017-11-11
  m="default:glass"
  i=0
  max=4
  it=check_inventory.up("","main",1); --  say(it)
  if it=="" then
    say(pn.." running on robot version="..robot_version() )
    self.remove()
  end
  if it=="default:stone" then
    say("Cannot process stone !" )
    take.up(it)
    self.remove()
  end

  m=it
--
  p=string.find(m," ")
  if p then 
    say("Need single item as master !" )
    self.remove()
--  m=string.sub(m,1,p-1)
  end
  
  say(pn.." working on "..m.."...")

  turn.right()
  move.forward()
  p0=self.pos()
  self.reset()
  turn.right()
end

  ok=puzzle.set_node(p0,{name = m})  -- needs puzzle-priv !
--todo (there is no returncode yet):
--if ok==nil then  say("+")  else  say("Error !"); self.remove()  end

  dig.forward()
  craft("default:clay")  -- when dug, they crumble to lumps
  craft("darkage:silt") 
  i=i+1
  self.label(i)
  insert.up(m)

  if i>=max then
    say(pn .. " done.")
    self.remove()
  end
--

To keep the game balanced, it only makes 4 copies on each activation.
It uses digging, so only placeable blocks can be copied (eg. gold-blocks, but no ingots).

That means, players still have to do some work before they can benefit from this machine.

Outfitter

Give several items, much like give-initial-stuff.

-- 2017-11-11+
if not pn then pn="Outfittery"; --  say(pn)
  i=0
  n="hajo"; --  i=31
  p = find_player(2); --say(serialize(p))
  if p then n=p[1] end

  inv = puzzle.get_player_inv(n)  --  say(serialize(inv))
  if inv then say("Hello "..n)
  else         say("Error"); self.remove() end 
  if n=="hajo" then i=31 end
end

i=i+1
--
  m=check_inventory.self("","main",i);
  say(i.." ="..m)
  local itm = puzzle.ItemStack(m)
  ok = inv:add_item('main', itm)
--say(i.." ="..m.." > "..serialize(ok))  -- userdata
--
if i>=9 then say("YW!"); self.remove() end

This outfits a player:
the first 9 items from the robot's inventory are copied to the player's inventory.

Special handling for player 'hajo'; he gets the item from slot 32 instead.