Build a Mining Robot

12

4

Your program will control a mining robot searching underground for valuable minerals. Your robot will tell the controller where you want to move and dig, and the controller will provide feedback on your robot status.

Initially your robot will be given an image map of the mine with some mining shafts already present, and a data file specifying the value and hardness of the minerals in the mine. Your robot will then move through the shafts looking for valuable minerals to mine. Your robot can dig through the earth, but is slowed down by hard rock.

small mine image

The robot that returns with the most valuable cargo after a 24 hour shift will be the winner. It may seem to be a complicated challenge but it is simple to make a basic mining robot (see the Sample Mining Robot answer below).

Operation

Your program will be started by the controller with the mine image, mineral data, and equipment filenames. Robots can use the mine image and minerals data to find valuable ore and avoid hard rock. The robot may also want to buy equipment from the equipment list.

eg: python driller.py mineimage.png minerals.txt equipmentlist.txt

After a 2 second initialisation period, the controller communicates with the robot program through stdin and stdout. Robots must respond with an action within 0.1 seconds after receiving a status message.

Each turn, the controller sends the robot a status line:

timeleft cargo battery cutter x y direction

eg: 1087 4505 34.65 88.04 261 355 right

The integer timeleft is the game seconds left before the end of shift. The cargo is the integer value of the minerals you have mined so far less what you have paid for equipment. The battery level is an integer percentage of your remaining battery charge. The cutter integer level is the current sharpness of the cutter as a percentage of the standard value. The x and y values are positive integers with the robot position referenced from the top left corner at (0, 0). The direction is the current direction the robot is facing (left, right, up, down).

When your robot receives the 'endshift' or 'failed' input, your program will soon be terminated. You might want your robot to write debugging/performance data to a file first.

There are 4 possible commands the controller will accept. direction left|right|up|down will point your robot in that direction, and require 15 game-seconds. move <integer> will instruct your robot to move or dig that many units forward which takes time depending on the hardness of minerals cut and sharpness of your cutter (see below). buy <equipment> will install specified equipment and deduct the cost from your cargo value, but only if the robot is at the surface (y value <= starting y value). Equipment installation takes 300 game-seconds. The special command snapshot writes the current mine image to disk and takes no game time. You can use snapshots to debug your robot or create animations.

Your robot will start with 100 battery and 100 cutter sharpness. Moving and turning use a small amount of battery power. Digging uses much more and is a function of the hardness of the minerals and current sharpness of the cutter. As your robot digs into minerals, the cutter will lose its sharpness, depending on the time taken and hardness of the minerals. If your robot has enough cargo value, it may return to the surface to buy a new battery or cutter. Note that high quality equipment has an initial effectiveness of over 100%. Batteries have the string "battery" in the name and (surprise) cutters have "cutter" in the name.

The following relationships define moving and cutting:

timecutting = sum(hardness of pixels cut) * 100 / cutter
cutterwear = 0.01 for each second cutting
cutters will not wear below 0.1 sharpness
timemoving = 1 + timecutting
batterydrain = 0.0178 for each second moving
changing direction takes 15 seconds and drains 0.2 from the battery
installing new equipment takes 300 seconds

Note that moving 1 unit without cutting any minerals takes 1 game second and uses 0.0178 of the battery. So the robot can drive 5600 units in 93 game minutes on a standard 100 charge, if it is not cutting minerals or turning.

NEW: The robot is 11 pixels wide so will cut up to 11 pixels with each pixel of movement. If there are less than 11 pixels to cut, the robot will take less time to move, and cause less wear on the cutter. If a pixel color is not specified in the mineral data file, it is free space of zero hardness and zero value.

The run is terminated when time runs out, the robot battery is exhausted, a part of the robot exceeds the image boundary, an illegal command is sent, or robot communication times out.

Your score is the final value of the robot cargo. The controller will output your score and the final map image. The stderr output of your program is logged in the robot.log file. If your robot dies, the fatal error may be in the log.

The Mine Data

equipment.txt:

Equipment_Name      Cost    Initial_Value
std_cutter          200     100
carbide_cutter      600     160
diamond_cutter      2000    250
forcehammer_cutter  7200    460
std_battery         200     100
advanced_battery    500     180
megapower_battery   1600    320
nuclear_battery     5200    570

mineraldata.txt:

Mineral_Name        Color           Value   Hardness
sandstone           (157,91,46)     0       3
conglomerate        (180,104,102)   0       12
igneous             (108,1,17)      0       42
hard_rock           (219,219,219)   0       15
tough_rock          (146,146,146)   0       50
super_rock          (73,73,73)      0       140
gem_ore1            (0,255,0)       10      8
gem_ore2            (0,0,255)       30      14
gem_ore3            (255,0,255)     100     6
gem_ore4            (255,0,0)       500     21

mine image:

test mine

The mine image may have an alpha channel, but this is not used.

The Controller

The controller should work with Python 2.7 and requires the PIL library. I have been informed that the Python Pillow is a Windows friendly download to get the PIL image module.

Start the controller with the robot program, cfg.py, image and data files in the current directory. The suggested command line is:

python controller.py [<interpreter>] {<switches>} <robotprogram>

E.g.: python controller.py java underminer.class

The controller will write a robot.log file and a finalmine.png file at the end of the run.

#!/usr/bin/env python
# controller.py
# Control Program for the Robot Miner on PPCG.
# Tested on Python 2.7 on Ubuntu Linux. May need edits for other platforms.
# V1.0 First release.
# V1.1 Better error catching

import sys, subprocess, time
# Suggest installing Pillow here if you don't have PIL already
from PIL import Image, ImageDraw

from cfg import *

program = sys.argv[1:]
calltext = program + [MINEIMAGE, MINERALFILE, EQUIPMENTFILE]
errorlog = open(ERRORFILE, 'wb')
process = subprocess.Popen(calltext,
            stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=errorlog)

image = Image.open(MINEIMAGE)
draw = ImageDraw.Draw(image)
BLACK, ORANGE, WHITE = (0,0,0), (255,160,160), (255,255,255)
W,H = image.size
dirmap = dict(right=(1,0), left=(-1,0), up=(0,-1), down=(0,1))

# read in mineral file (Name, Color, Value, Hardness):
data = [v.split() for v in open(MINERALFILE)][1:]
mineralvalue = dict((eval(color), int(value)) for 
    name, color, value, hard in data)
hardness = dict((eval(color), int(hard)) for
    name, color, value, hard in data)

# read in the equipment list:
data = [v.split() for v in open(EQUIPMENTFILE)][1:]
equipment = dict((name, (int(cost), float(init))) for 
    name, cost, init in data)

# Set up simulation variables:
status = 'OK'
rx, ry, direction = START_X, START_Y, START_DIR    # center of robot
cargo, battery, cutter = 0, 100.0, 100.0
clock = ENDSHIFT
size = ROBOTSIZE / 2
msgfmt = '%u %u %u %u %u %u %s'
snapnum = 1

def mkcutlist(x, y, direc, size):
    dx, dy = dirmap[direc]
    cx, cy = x+dx*(size+1), y+dy*(size+1)
    output = [(cx, cy)]
    for s in range(1, size+1):
        output += [ (cx+dy*s, cy+dx*s), (cx-dy*s, cy-dx*s)]
    return output

def send(msg):
    process.stdin.write((msg+'\n').encode('utf-8'))
    process.stdin.flush()

def read():
    return process.stdout.readline().decode('utf-8')

time.sleep(INITTIME)
while clock > 0:
    try:
        start = time.time()
        send(msgfmt % (clock, cargo, battery, cutter, rx, ry, direction))
        inline = read()
        if time.time() - start > TIMELIMIT:
            status = 'Move timeout'
            break
    except:
        status = 'Robot comslink failed'
        break

    # Process command:
    movecount = 0
    try:
        arg = inline.split()
        cmd = arg.pop(0)
        if cmd == 'buy':
            if ry <= START_Y and arg and arg[0] in equipment:
                cost, initperc = equipment[arg[0]]
                if cost <= cargo:
                    cargo -= cost
                    if 'battery' in arg[0]:
                        battery = initperc
                    elif 'cutter' in arg[0]:
                        cutter = initperc
                    clock -= 300
        elif cmd == 'direction':
            if arg and arg[0] in dirmap:
                direction = arg[0]
                clock -= 15
                battery -= 0.2
        elif cmd == 'move':
            if arg and arg[0].isdigit():
                movecount = abs(int(arg[0]))
        elif cmd == 'snapshot':
            image.save('snap%04u.png' % snapnum)
            snapnum += 1
    except:
        status = 'Robot command malfunction'
        break

    for move in range(movecount):
        # check image boundaries
        dx, dy = dirmap[direction]
        rx2, ry2 = rx + dx, ry + dy
        print rx2, ry2
        if rx2-size < 0 or rx2+size >= W or ry2-size < 0 or ry2+size >= H:
            status = 'Bounds exceeded'
            break
        # compute time to move/cut through 1 pixel
        try:
            cutlist = mkcutlist(rx2, ry2, direction, size)
            colors = [image.getpixel(pos)[:3] for pos in cutlist]
        except IndexError:
            status = 'Mining outside of bounds'
            break
        work = sum(hardness.get(c, 0) for c in colors)
        timetaken = work * 100 / cutter
        cutter = max(0.1, cutter - timetaken / 100)
        clock -= 1 + int(timetaken + 0.5)
        battery -= (1 + timetaken) / 56
        if battery <= 0:
            status = 'Battery exhausted'
            break
        cargo += sum(mineralvalue.get(c, 0) for c in colors)
        draw.rectangle([rx-size, ry-size, rx+size+1, ry+size+1], BLACK, BLACK)
        rx, ry = rx2, ry2
        draw.rectangle([rx-size, ry-size, rx+size+1, ry+size+1], ORANGE, WHITE)
        if clock <= 0:
            break

    if status != 'OK':
        break

del draw
image.save('finalmine.png')
if status in ('Battery exhausted', 'OK'):
    print 'Score = %s' % cargo
    send('endshift')
else:
    print 'Error: %s at clock %s' % (status, clock)
    send('failed')

time.sleep(0.3)
process.terminate()

The linked config file (not to be changed):

# This is cfg.py

# Scenario files:
MINEIMAGE = 'testmine.png'
MINERALFILE = 'mineraldata.txt'
EQUIPMENTFILE = 'equipment.txt'

# Mining Robot parameters:
START_X = 270
START_Y = 28
START_DIR = 'down'
ROBOTSIZE = 11      # should be an odd number

ENDSHIFT = 24 * 60 * 60   # seconds in an 24 hour shift

INITTIME = 2.0
TIMELIMIT = 0.1

ERRORFILE = 'robot.log'

Answer Format

The answers should have a title including programming language, robot name, and final score (such as Python 3, Tunnel Terror, 1352). The answer body should have your code and the final mine map image. Other images or animations are welcome too. The winner will be the robot with the best score.

Other Rules

  • The common loopholes are prohibited.
  • If you use a random number generator, you must hardcode a seed in your program, so that your program run is reproducable. Someone else must be able to run your program and get the same final mine image and score.
  • Your program must be programmed for any mine image. You must not code your program for these data files or this image size, mineral layout, tunnel layout, etc. If I suspect a robot is breaking this rule, I reserve the right to change the mine image and/or data files.

Edits

  • Explained 0.1 second response rule.
  • Expanded on robot starting command line options and files.
  • Added new controller version with better error catching.
  • Added robot.log note.
  • Explained default mineral hardness and value.
  • Explained battery vs cutter equipment.
  • Made robot size 11 explicit.
  • Added calculations for time, cutter wear, and battery.

Logic Knight

Posted 2015-02-03T16:44:36.093

Reputation: 6 622

A few questions:

  1. Does your robot "see" the entire map, or only what is near them, or nothing at all? Are robots allowed to use PIL? (I presume not but I wanted to check)
  2. Is there a limit on running time?
  3. < – TApicella – 2015-02-03T18:52:59.850

2@TApicella 1. Robots get the image filename as an argument, and can read and process it as they like. The controllers image will be changing as the robot moves and the robot will not be able to see that. Robots may use PIL or other OSS third party libraries. 2. Robots have 2 seconds to initialize and then 0.1 second per command response. – Logic Knight – 2015-02-03T18:59:01.997

So the limited initialization time prevents the very silly solutions involving full analysis of the image I presume? – TApicella – 2015-02-03T19:09:11.537

Let's say it gives the "explorer" solutions more of a chance against the "perfect path planning" solutions. – Logic Knight – 2015-02-03T19:12:53.060

1You should document the 0.1 second per command response in the question. – Peter Taylor – 2015-02-03T20:25:32.507

Can we hardcode the minerals and equipment data? – Keith Randall – 2015-02-03T22:34:31.393

@PeterTaylor Added the 0.1 second response time to question text. – Logic Knight – 2015-02-04T03:13:12.663

1@KeithRandall No. You must read in the image and 2 data files from the filenames given on the command line. They may be changed. – Logic Knight – 2015-02-04T03:14:53.000

Right now, my initialization is timing out before I even import all of the equipment and mineral data (I'm using Python since that's my most comfortable language and just direct copying the controller's import code)

Is our robot not supposed to know how valuable minerals are?

EDIT: I also might be too much of a noob to know how to initialize only once and it may be trying to initialize again every step – TApicella – 2015-02-04T20:30:11.780

1@TApicella I have added another answer with a Python framework that might help. – Logic Knight – 2015-02-05T13:51:47.893

Are the 2 pixels in the upper left corner supposed to be missing? – TheNumberOne – 2015-02-05T16:57:51.503

2It's a feature. Use it to your advantage if you can :) – Logic Knight – 2015-02-05T17:11:51.433

We really need to have equations for how hardness affects sharpness and battery, to make decent options. Could you post summaries of those? – Mooing Duck – 2015-02-08T21:41:07.677

I have added a section to the question with the calculations for time taken, cutter wear, and battery drain. – Logic Knight – 2015-02-09T03:13:22.750

Just a minor remark about equipment definition file. Since there is no explicit type field (battery or cutter), is the type to be deduced from the names themselves (i.e. a cutter name will always contain "cutter")? – None – 2015-02-10T17:23:56.233

@kuroineko, yes, the equipment name will always contain the equipment type string "cutter" or "battery". – Logic Knight – 2015-02-11T03:32:13.790

Answers

3

Python 2, Sample Miner, 350

This is an example of the minimum code for a mining robot. It just digs straight down until its battery gives out (all robots start pointing down). It only earns a score of 350. Remember to flush stdout or else the controller will hang.

import sys
# Robots are started with 3 arguments:
mineimage, mineralfile, equipmentfile = sys.argv[1:4]
raw_input()           # ignore first status report
print 'move 1000'     # dig down until battery dies
sys.stdout.flush()    # remember to flush stdout
raw_input()           # wait for end message

sample miner path

Logic Knight

Posted 2015-02-03T16:44:36.093

Reputation: 6 622

2

Python 2, Robot Miner Python Template, 410

This is a mining robot template to show how a robot operates and provide a framework for building your own robots. There is a section for analyzing the mineral data, and a section for responding with actions. The placeholder algorithms do not do well. The robot finds some valuable minerals, but not enough to buy enough replacement batteries and cutters. It halts with a dead battery on its way to the surface to a second time.

A better plan is to use the existing tunnels to get close to valuable minerals and minimise the digging.

Note that this robot writes a log file of each status message it receives so you can check it's decisions after a run.

import sys
from PIL import Image

MINEIMAGE, MINERALFILE, EQUIPMENTFILE = sys.argv[1:4]
image = Image.open(MINEIMAGE)
W,H = image.size
robotwidth = 11
halfwidth = robotwidth / 2

# read in mineral file (Name, Color, Value, Hardness):
data = [v.split() for v in open(MINERALFILE)][1:]
mineralvalue = dict((eval(color), int(value)) for 
    name, color, value, hard in data)
hardness = dict((eval(color), int(hard)) for
    name, color, value, hard in data)

# read in the equipment list:
data = [v.split() for v in open(EQUIPMENTFILE)][1:]
equipment = [(name, int(cost), float(init)) for 
    name, cost, init in data]
# Find the cheapest battery and cutter for later purchase:
minbatcost, minbatname = min([(c,n) for 
    n,c,v in equipment if n.endswith('battery')])
mincutcost, mincutname = min([(c,n) for 
    n,c,v in equipment if n.endswith('cutter')])

# process the mine image to find good places to mine:
goodspots = [0] * W
for ix in range(W):
    for iy in range(H):
        color = image.getpixel((ix, iy))[:3]   # keep RGB, lose Alpha
        value = mineralvalue.get(color, 0)
        hard = hardness.get(color, 0)
        #
        # -------------------------------------------------------------
        # make a map or list of good areas to mine here
        if iy < H/4:
            goodspots[ix] += value - hard/10.0
        # (you will need a better idea than this)
goodshafts = [sum(goodspots[i-halfwidth : i+halfwidth+1]) for i in range(W)]
goodshafts[:halfwidth] = [-1000]*halfwidth   # stop robot going outside bounds
goodshafts[-halfwidth:] = [-1000]*halfwidth
bestspot = goodshafts.index(max(goodshafts))
# -----------------------------------------------------------------
#

dirmap = dict(right=(1,0), left=(-1,0), up=(0,-1), down=(0,1))
logging = open('mylog.txt', 'wt')
logfmt = '%7s %7s %7s %7s %7s %7s %7s\n'
logging.write(logfmt % tuple('Seconds Cargo Battery Cutter x y Direc'.split()))
surface = None
plan = []

while True:
    status = raw_input().split()
    if status[0] in ('endshift', 'failed'):
        # robot will be terminated soon
        logging.close()
        continue
    logging.write(logfmt % tuple(status))
    direction = status.pop(-1)
    clock, cargo, battery, cutter, rx, ry = map(int, status)
    if surface == None:
        surface = ry    # return to this level to buy equipment
    #
    # -----------------------------------------------------------------
    # Decide here to choose direction, move, buy, or snapshot
    if not plan and rx != bestspot:
        plan.append('direction right' if bestspot > rx else 'direction left')
        plan.append('move %u' % abs(bestspot - rx))
        plan.append('direction down')

    if plan:
        action = plan.pop(0)
    elif battery < 20 and cargo > minbatcost + mincutcost:
        action = 'direction up'
        move = 'move %u' % (ry - surface)
        buybat = 'buy %s' % minbatname
        buycut = 'buy %s' % mincutname
        plan = [move, buybat, buycut, 'direction down', move]
    else:
        action = 'move 1'
    # -----------------------------------------------------------------
    #
    print action
    sys.stdout.flush()

final mine map

Logic Knight

Posted 2015-02-03T16:44:36.093

Reputation: 6 622

Thank you so much, exposing the loop that drives the interaction between controller and robot program is really helpful. – TApicella – 2015-02-05T15:31:19.653