14
\$\begingroup\$

Scriptbot Warz!


The results are in and Assassin is our champion, winning 2 of 3 matches! Thanks to everyone who submitted their Scriptbots! Special thanks to horns for BestOpportunityBot which displayed excellent pathing and made full use of all action options.

Map 1

Assassin took out BestOpportunityBot early on, and the rest of the match was pretty boring. Detailed play-by-play here.

  1. Assassin: 10 HP, 10 Damage Dealt, 3 Damage Taken
  2. The Avoider v3: 10 HP, 0 Damage Dealt, 0 Damage Taken
  3. Gotta Finish Eating: 10 HP, 0 Damage Dealt, 0 Damage Taken
  4. BestOpportunityBot: 0 HP, 3 Damage Dealt, 10 Damage Taken

Map 2

BestOpportunityBot did most of the work on this match, but Assassin was able to take him out in the end. Detailed play-by-play here.

  1. Assassin: 2 HP, 10 Damage Dealt, 9 Damage Taken
  2. BestOpportunityBot: 0 HP, 32 Damage Dealt, 10 Damage Taken
  3. The Avoider v3: 0 HP, 0 Damage Dealt, 12 Damage Taken
  4. Gotta Finish Eating: 0 HP, 0 Damage Dealt, 11 Damage Taken

Map 3

BestOpportunityBot pushed everyone into traps on this match. Very cool. Detailed play-by-play here.

  1. BestOpportunityBot: 10 HP, 30 Damage Dealt, 0 Damage Taken
  2. Assassin: 0 HP, 0 Damage Dealt, 0 Damage Taken
  3. Gotta Finish Eating: 0 HP, 0 Damage Dealt, 0 Damage Taken
  4. The Avoider v3: 0 HP, 0 Damage Dealt, 0 Damage Taken

Thanks for your answers! Since there are just 4 Scriptbots, we are abandoning the tournament plans for three free-for-all matches, one on each of the maps below. The scriptbot with the highest win record wins. In the event of a tie, we will enter into sudden death wherein the scriptbot who breaks the tie first wins.


Your task, should you choose to accept it, is to code a Scriptbot which can traverse an ASCII map and destroy its opponents. Each battle will take the form of a random-starting-order turn-based game where each Scriptbot has a chance to spend their energy points (EP) to take actions. The GameMaster script will feed input to, and interpret output from each Scriptbot.

Environment

Each Scriptbot is contained within its own directory where it can read from the map and stats files and read/write to the data file. The data file can be used to store any persistent information you might find useful.

The stats File

The stats file contains information about your opponents and is formatted as follows. Each player is represented on a separate row. The first column is a player ID (@ means you). The second column is the health of that player.

1,9HP
@,10HP
3,9HP
4,2HP

The map File

The map file might look something like this...

####################
#   #          #   #
# 1 #          # 2 #
#                  #
###              ###
#                  #
#      #           #
#       #     !    #
#        #         #
#       !####      #
#      ####!       #
#         #        #
#    !     #       #
#           #      #
#                  #
###              ###
#                  #
# 3 #          # @ #
#   #          #   #
####################

... or this...

######################################
#       # 1        #   @             #
#       #          #          #!     #
#       #          #          ####   #
#  #    #  #       #         !#!     #
#  #    #  #       #      #####      #
#  ###     #    ####    #         #  #
#          #    #!      ###       #  #
#  ######  #    #       #     #####  #
#  #!      #    ######  #        !#  #
#  ###     #            #         #  #
#  #    2  #            #   4     #  #
######################################

... or this...

###################
###!!!!!!#!!!!!!###
##!             !##
#! 1     !     2 !#
#!       !       !#
#!               !#
#!               !#
#!      !!!      !#
## !!   !!!   !! ##
#!      !!!      !#
#!               !#
#!               !#
#!       !       !#
#! 3     !     @ !#
##!             !##
###!!!!!!#!!!!!!###
###################

... or it might look totally different. Either way, the characters used and their meaning will remain the same:

  • # A wall, impassable and impenetrable.
  • 1, 2, 3... A number representing an enemy player. These numbers correspond to the player ID in the stats file.
  • ! A trap. Scriptbots who move onto these locations will die immediately.
  • @ Your Scriptbot's location.
  • Open space which you are free to move around in.

Gameplay

The GameMaster script will assign a random starting order to the Scriptbots. The Scriptbots are then invoked in this order while they are still alive. Scriptbots have 10 Health Points (HP), and start with 10 Energy Points (EP) each turn, which they may use to move or attack. At the start of each turn, a Scriptbot will heal for one HP, or be granted one additional EP if already at 10 HP (thus running may be a viable strategy at times).

The battle ends when only one Scriptbot survives or when 100 turns have passed. If multiple Scriptbots are alive at the end of a battle, their place is determined based on the following criteria:

  1. Most health.
  2. Most damage dealt.
  3. Most damage taken.

Scriptbot Input

The GameMaster will print the battle map to a file named map which the Scriptbot will have access to read from. The map might take any form, so it is important that the Scriptbot be able to interpret it. Your Scriptbot will be invoked with one parameter indicating EP. For example...

:> example_scriptbot.py 3

The Scriptbot will be invoked until it spends all of its EP or a maximum of 10 11 times. The map and stats files are updated before each invocation.

Scriptbot Output

Scriptbots should output their actions to stout. A list of possible actions are as follows:

  • MOVE <DIRECTION> <DISTANCE>

    Costs 1 EP per DISTANCE. The MOVE command moves your Scriptbot around the map. If there is something in the way such as a wall or another Scriptbot, the GameMaster will move your Scriptbot as far as possible. If a DISTANCE greater than the Scriptbot's remaining EP is given, the GameMaster will move the Scriptbot until its EP runs out. DIRECTION may be any compass direction of N, E, S, or W.

  • PUSH <DIRECTION> <DISTANCE>

    Costs 1 EP per DISTANCE. The PUSH command enables a Scriptbot to move another Scriptbot. The Scriptbot issuing the command must be directly next to the Scriptbot being pushed. Both Scriptbots will move in the direction indicated if there is not an object blocking the Scriptbot being pushed. DIRECTION and DISTANCE are the same as for the MOVE command.

  • ATTACK <DIRECTION>

    Costs one EP. The ATTACK command deals 1 damage to any Scriptbot directly next to the issuing Scriptbot and in the direction specified. DIRECTION is the same as for the MOVE command.

  • PASS

    Ends your turn.

Supported Languages

To keep this reasonable for me, I will accept the following languages:

  • Java
  • Node.js
  • Python
  • PHP

You are limited to libraries which are commonly packaged with your languages out of the box. Please do not make me locate obscure libraries to make your code work.

Submission and Judging

Post your Scriptbot source code below and give it a cool name! Please also list the version of the language you used. All Scriptbots will be reviewed for tomfoolery so please comment well and do not obfuscate your code.

You may submit more than one entry, but please make them totally unique entries, and not versions of the same entry. For instance, you may want to code a Zerg Rush bot and a Gorilla Warfare bot. That's fine. Do not post Zerg Rush v1, Zerg Rush v2, etc.

On November 7th I will collect all answers and those which pass the initial review will be added to a tournament bracket. The champion gets the accepted answer. The ideal bracket is shown below. Since there will likely not be exactly 16 entries, some brackets may end up being only three or even two bots. I will try to make the bracket as fair as possible. Any necessary favoritism (in the event that a bye week is needed for instance) will be given to the bots which were submitted first.

BOT01_
BOT02_|
BOT03_|____
BOT04_|    |
           |
BOT05_     |
BOT06_|___ |
BOT07_|  | |
BOT08_|  | |_BOT ?_
         |___BOT ?_|
BOT09_    ___BOT ?_|___CHAMPION!
BOT10_|  |  _BOT ?_|
BOT11_|__| |
BOT12_|    |
           |
BOT13_     |
BOT14_|____|
BOT15_|
BOT16_|

Q&A

I'm sure I've missed some details, so feel free to ask questions!

May we trust that a map file is always surrounded by # symbols? If not, what happens in the event that a bot attempts to walk off the map? - BrainSteel

Yes the map will always be bounded by # and your Scriptbot will start inside of these bounds.

If there is no bot present in the direction specified in a PUSH command, how does the command function? - BrainSteel

The GameMaster will do nothing, zero EP will be spent, and the Scriptbot will be called again.

Do unused EP accumulate? - feersum

No. Each Scriptbot will start the round/turn with 10 EP. Any EP not spent will go to waste.

I think I've got it, but just to clarify: with bots A and B, is the order of events A@10EP->MOVE MAP_UPDATE B@10EP->PUSH MAP_UPDATE A@9EP->ATTACK MAP_UPDATE B@9EP->ATTACK ..., or A@10EP->MOVE A@9EP->ATTACK ... MAP_UPDATE B@10EP->PUSH B@9EP->ATTACK ... MAP_UPDATE? In other words, is all action in one controller-bot query loop atomic? If so, why the loop? Why not return a single file with all the actions to be completed? Otherwise bots will have to write out their own state files to keep track of multi-action sequences. The map/stats file will only be valid prior to the first action. - COTO

Your second example is close, but not quite right. During a turn, the Scriptbot is invoked repeatedly until their EP is spent, or a maximum of 11 times. The map and stats files are updated before each invocation. The loop is useful in the event that a bot gives invalid output. The GameMaster will deal with the invalid output and involke the bot again, giving the bot a chance to correct for it's mistake.

will you release the GameMaster script for testing? - IchBinKeinBaum

The GameMaster script will not be released. I would encourage you to create a map and stats file to test your bot's behavior.

If robotA pushes robotB into a trap, is robotA credited with "damage dealt" points equal to the current health of robotB? - Mike Sweeney

Yes, that is a good idea. A bot will be granted damage points equal to the health of a any bot that it pushes into a trap.

\$\endgroup\$
  • \$\begingroup\$ May we trust that a map file is always surrounded by # symbols? If not, what happens in the event that a bot attempts to walk off the map? \$\endgroup\$ – BrainSteel Oct 30 '14 at 20:49
  • \$\begingroup\$ @BrainSteel Yes the map will always be bounded by # and your Scriptbot will start inside of these bounds. \$\endgroup\$ – Rip Leeb Oct 30 '14 at 20:50
  • 3
    \$\begingroup\$ If you're sure you've missed something, why not post it in the sandbox? \$\endgroup\$ – Martin Ender Oct 30 '14 at 20:53
  • 2
    \$\begingroup\$ @MartinBüttner I've thought this through quite thoroughly. I was just trying to be friendly and make it clear that questions are welcome. \$\endgroup\$ – Rip Leeb Oct 30 '14 at 20:57
  • 1
    \$\begingroup\$ If robotA pushes robotB into a trap, is robotA credited with "damage dealt" points equal to the current health of robotB? \$\endgroup\$ – Logic Knight Oct 31 '14 at 17:34
1
\$\begingroup\$

Assassin (Java 1.7)

Tries to kill enemies whenever possible, otherwise moves one field. It is pretty good in finding the path to an enemy, but doesn't do anything to hide from other bots.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Assassin {
    private final Path dataPath = Paths.get("data");
    private final Path mapPath = Paths.get("map");
    private final Path statsPath = Paths.get("stats");
    private final List<Player> players = new ArrayList<>();
    private final int energy;
    private Map map = null;

    public Assassin(int energy) {
        this.energy = energy;
    }

    private void doSomething() {
        if (dataFileEmpty()) {
            calculateTurn();
        }
        printStoredOutput();
    }

    private boolean dataFileEmpty() {
        try {
            return !Files.exists(dataPath) || Files.size(dataPath)  == 0;
        } catch (IOException e) {
            return true;
        }
    }

    private void printStoredOutput() {
        try {
            List<String> lines = Files.readAllLines(dataPath, StandardCharsets.UTF_8);
            //print first line
            System.out.println(lines.get(0));           
            //delete first line
            lines.remove(0);
            Files.write(dataPath, lines, StandardCharsets.UTF_8);
        } catch (IOException e) {
            System.out.println("PASS");
        }
    }

    private void calculateTurn() {
        try {
            readStats();
            readMap();
            if (!tryKill())  {
                sneakCloser();
            }
        } catch (IOException e) {}
    }

    private void readStats() throws IOException{
        List<String> stats = Files.readAllLines(statsPath, StandardCharsets.UTF_8);
        for (String stat : stats) {
            String[] infos = stat.split(",");
            int hp = Integer.parseInt(infos[1].replace("HP", ""));
            players.add(new Player(stat.charAt(0), hp));
        }
    }

    private void readMap() throws IOException{
        List<String> lines = Files.readAllLines(mapPath, StandardCharsets.UTF_8);
        Field[][] fields = new Field[lines.size()][lines.get(0).length()];
        for (int row = 0; row < lines.size(); row++) {
            String line = lines.get(row);
            for (int col = 0; col < line.length(); col++) {
                fields[row][col] = new Field(line.charAt(col), row, col);
            }
        }
        map = new Map(fields);
    }

    private boolean tryKill() {     
        Field me = map.getMyField();
        for (Field field : map.getEnemyFields()) {
            for (Field surrField : map.surroundingFields(field)) { 
                List<Direction> dirs = map.path(me, surrField);
                if (dirs != null) {
                    for (Player player : players) {
                        //can kill this player
                        int remainderEnergy = energy - dirs.size() - player.hp;
                        if (player.id == field.content && remainderEnergy >= 0) {
                            //save future moves
                            List<String> commands = new ArrayList<>();
                            for (Direction dir : dirs) {
                                commands.add("MOVE " + dir + " 1");
                            }
                            //attacking direction
                            Direction attDir = surrField.dirsTo(field).get(0);
                            for (int i = 0; i < player.hp; i++) {
                                commands.add("ATTACK " + attDir);                               
                            }
                            if (remainderEnergy > 0) {
                                commands.add("PASS");
                            }
                            try {
                                Files.write(dataPath, commands, StandardCharsets.UTF_8);
                            } catch (IOException e) {}
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    private void sneakCloser() {        
        Field me = map.getMyField();
        for (Direction dir : Direction.values()) {
            if (!map.move(me, dir).blocked()) {
                List<String> commands = new ArrayList<>();
                commands.add("MOVE " + dir + " 1");
                commands.add("PASS");
                try {
                    Files.write(dataPath, commands, StandardCharsets.UTF_8);
                } catch (IOException e) {}
                return;
            }
        }
    }

    public static void main(String[] args) {
        try {
            new Assassin(Integer.parseInt(args[0])).doSomething();
        } catch (Exception e) {
            System.out.println("PASS");
        }
    }

    class Map {
        private Field[][] fields;

        public Map(Field[][] fields) {
            this.fields = fields;
        }

        public Field getMyField() {
            for (Field[] rows : fields) {
                for (Field field : rows) {
                    if (field.isMyPos()) {
                        return field;
                    }
                }
            }
            return null; //should never happen
        }   

        public List<Field> getEnemyFields() {
            List<Field> enemyFields = new ArrayList<>();
            for (Field[] rows : fields) {
                for (Field field : rows) {
                    if (field.hasEnemy()) {
                        enemyFields.add(field);
                    }
                }
            }
            return enemyFields;
        }

        public List<Field> surroundingFields(Field field) {
            List<Field> surrFields = new ArrayList<>();
            for (Direction dir : Direction.values()) {
                surrFields.add(move(field, dir));
            }
            return surrFields;
        }

        public Field move(Field field, Direction dir) {
            return fields[field.row + dir.rowOffset][field.col + dir.colOffset];
        }

        public List<Direction> path(Field from, Field to) {
            List<Direction> dirs = new ArrayList<>();
            Field lastField = from;
            boolean changed = false;

            for (int i = 0; i < energy && lastField != to; i++) {
                List<Direction> possibleDirs = lastField.dirsTo(to);
                changed = false;
                for (Direction dir : possibleDirs) {
                    Field nextField = move(lastField, dir);
                    if (!nextField.blocked()) {
                        lastField = nextField;
                        changed = true;
                        dirs.add(dir);
                        break;
                    }
                }
                if (!changed) {
                    return null; //not possible
                }
            }
            if (lastField != to) {
                return null; //not enough energy
            }           
            return dirs;
        }
    }

    class Field {
        private char content;
        private int row;
        private int col;

        public Field(char content, int row, int col) {
            this.content = content;
            this.row = row;
            this.col = col;
        }

        public boolean hasEnemy() {
            return content == '1' || content == '2' || content == '3' || content == '4';
        }

        public List<Direction> dirsTo(Field field) {
            List<Direction> dirs = new ArrayList<>();
            int distance = Math.abs(row - field.row) + Math.abs(col - field.col);

            for (Direction dir : Direction.values()) {
                int dirDistance = Math.abs(row - field.row + dir.rowOffset) + 
                        Math.abs(col - field.col + dir.colOffset);
                if (dirDistance < distance) {
                    dirs.add(dir);
                }
            }
            if (dirs.size() == 1) { //add near directions
                for (Direction dir : dirs.get(0).nearDirections()) {
                    dirs.add(dir);
                }
            }
            return dirs;
        }

        public boolean isMyPos() {
            return content == '@';
        }

        public boolean blocked() {
            return content != ' ';
        }
    }

    class Player {
        private char id;
        private int hp;

        public Player(char id, int hp) {
            this.id = id;
            this.hp = hp;
        }
    }

    enum Direction {
        N(-1, 0),S(1, 0),E(0, 1),W(0, -1);

        private final int rowOffset;
        private final int colOffset;

        Direction(int rowOffset, int colOffset) {
            this.rowOffset = rowOffset;
            this.colOffset = colOffset;
        }

        public Direction[] nearDirections() {
            Direction[] dirs = new Direction[2];
            for (int i = 0; i < Direction.values().length; i++) {
                Direction currentDir = Direction.values()[i];
                if (Math.abs(currentDir.rowOffset) != Math.abs(this.rowOffset)) {
                    dirs[i%2] = currentDir;
                }
            }
            return dirs;
        }
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ What version of Java was this written in? \$\endgroup\$ – Rip Leeb Nov 7 '14 at 16:26
  • \$\begingroup\$ @Nate I used 1.7. \$\endgroup\$ – CommonGuy Nov 7 '14 at 16:33
3
\$\begingroup\$

The Avoider v3

A simple minded bot. It is afraid of other robots and traps. It will not attack. It ignores the stats file and does not think ahead at all.

This is mainly a test to see how the rules would work, and as a dumb opponent for other competitors.

Edit: Now will PASS when no MOVE is better.

Edit2: Robots can be '1234' instead of '123'

The Python Code:

import sys
maxmoves = int(sys.argv[1])

ORTH = [(-1,0), (1,0), (0,-1), (0,1)]
def orth(p):
    return [(p[0]+dx, p[1]+dy) for dx,dy in ORTH]

mapfile = open('map').readlines()[::-1]
arena = dict( ((x,y),ch) 
    for y,row in enumerate(mapfile)
    for x,ch in enumerate(row.strip()) )
loc = [loc for loc,ch in arena.items() if ch == '@'][0]

options = []
for direc in ORTH:
    for dist in range(maxmoves+1):
        newloc = (loc[0]+direc[0]*dist, loc[1]+direc[1]*dist)
        if arena.get(newloc) in '#!1234':
            break
        penalty = dist * 10  # try not to use moves too fast
        if newloc == loc:
            penalty += 32   # incentive to move
        for nextto in orth(newloc):
            ch = arena.get(nextto)
            if ch == '#':
                penalty += 17  # don't get caught againt a wall
            elif ch in '1234':
                penalty += 26  # we are afraid of other robots
            elif ch == '!':
                penalty += 38  # we are very afraid of deadly traps
        options.append( [penalty, dist, direc] )

penalty, dist, direc = min(options)
if dist > 0:
    print 'MOVE', 'WESN'[ORTH.index(direc)], dist
else:
    print 'PASS'  # stay still?
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ elif ch in '123': You want to look for at least 1234. If you are bot 3, the opponent list would be 124. \$\endgroup\$ – Rip Leeb Oct 31 '14 at 18:27
  • 1
    \$\begingroup\$ @Nate Thanks. I have changed the bot. You might want to make this clear in the rules. I might not be the only one that has misunderstood this. \$\endgroup\$ – Logic Knight Nov 1 '14 at 4:48
3
\$\begingroup\$

BestOpportunityBot

This ended up being a little longer than I intended...and I'm not sure if I understand the rules for turns completely, so we'll see how this does.

from sys import argv
from enum import IntEnum

with open("map") as map_file:
    map_lines = map_file.read().splitlines()

with open("stats") as stats_file:
    stats_data = stats_file.read().splitlines()

ep = argv[1]

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(lhs, rhs):
        if (type(rhs) == tuple or type(rhs) == list):
            return Point(lhs.x + rhs[0], lhs.y + rhs[1])
        return Point(lhs.x + rhs.x, lhs.y + rhs.y)

    def __sub__(lhs, rhs):
        if (type(rhs) == tuple or type(rhs) == list):
            return Point(lhs.x - rhs[0], lhs.y - rhs[1])
        return Point(lhs.x - rhs.x, lhs.y - rhs.y)

    def __mul__(lhs, rhs):
        return Point(lhs.x * rhs, lhs.y * rhs)

    def __str__(self):
        return "(" + str(self.x) + ", " + str(self.y) + ")"

    def __repr__(self):
        return "(" + str(self.x) + ", " + str(self.y) + ")"

    def __eq__(lhs, rhs):
        return lhs.x == rhs.x and lhs.y == rhs.y

    def __hash__(self):
        return hash(self.x) * 2**32 + hash(self.y)

    def reverse(self):
        return Point(self.y, self.x)

dirs = (Point(0, 1), Point(1, 0), Point(0, -1), Point(-1, 0))

class Bot:
    def __init__(self, pos, ch, hp):
        self.pos = pos
        self.ch = ch
        self.hp = hp

    def __str__(self):
        return str(self.pos) + " " + str(self.ch) + " " + str(self.hp)

    def __repr__(self):
        return str(self.pos) + " " + str(self.ch) + " " + str(self.hp)

class MyBot(Bot):
    def __init__(self, pos, ch, hp, ep):
        self.ep = ep
        super().__init__(pos, ch, hp)

    def __str__(self):
        return super().__str__() + " " + self.ep

    def __repr__(self):
        return super().__repr__() + " " + self.ep

class Arena:
    def __init__(self, orig_map):
        self._map = list(zip(*orig_map[::-1]))
        self.bots = []
        self.me = None

    def __getitem__(self, indexes):
        if (type(indexes) == Point):
            return self._map[indexes.x][indexes.y]
        return self._map[indexes[0]][indexes[1]]

    def __str__(self):
        output = ""
        for i in range(len(self._map[0]) - 1, -1, -1):
            for j in range(len(self._map)):
                output += self._map[j][i]
            output += "\n"
        output = output[:-1]
        return output

    def set_bot_loc(self, bot):
        for x in range(len(self._map)):
            for y in range(len(self._map[x])):
                if self._map[x][y] == bot.ch:
                    bot.pos = Point(x, y)

    def set_bots_locs(self):
        self.set_bot_loc(self.me)
        for bot in self.bots:
            self.set_bot_loc(bot)

    def adjacent_bots(self, pos=None):
        if type(pos) == None:
            pos = self.me.pos
        output = []
        for bot in self.bots:
            for d in dirs:
                if bot.pos == pos + d:
                    output.append(bot)
                    break
        return output

    def adjacent_bots_and_dirs(self):
        output = {}
        for bot in self.bots:
            for d in dirs:
                if bot.pos == self.me.pos + d:
                    yield bot, d
                    break
        return output

    def look(self, position, direction, distance, ignore='', stopchars='#1234'):
        current = position + direction
        output = []
        for i in range(distance):
            if (0 <= current.x < len(self._map) and
                0 <= current.y < len(self._map[current.x]) and
                (self[current] not in stopchars or self[current] in ignore)):
                output += self[current]
                current = current + direction
            else:
                break
        return output

    def moveable(self, position, direction, distance):
        current = position + direction
        output = []
        for i in range(distance):
            if (0 <= current.x < len(self._map) and
                0 <= current.y < len(self._map[current.x]) and
                self[current] == ' '):
                output += self[current]
            else:
                break
        return output


    def danger(self, pos, ignore=None): # damage that can be inflicted on me
        output = 0
        adjacents = self.adjacent_bots(pos)
        hps = [bot.hp for bot in adjacents if bot != ignore]
        if len(hps) == 0:
            return output

        while max(hps) > 0:
            if 0 in hps:
                hps.remove(0)

            for i in range(len(hps)):
                if hps[i] == min(hps):
                    hps[i] -= 1
                output += 1
        return output

    def path(self, pos): # Dijkstra's algorithm adapted from https://gist.github.com/econchick/4666413
        visited = {pos: 0}
        path = {}

        nodes = set()

        for i in range(len(self._map)):
            for j in range(len(self._map[0])):
                nodes.add(Point(i, j))

        while nodes:
            min_node = None
            for node in nodes:
                if node in visited:
                    if min_node is None:
                        min_node = node
                    elif visited[node] < visited[min_node]:
                        min_node = node

            if min_node is None:
                break

            nodes.remove(min_node)
            current_weight = visited[min_node]

            for _dir in dirs:
                new_node = min_node + _dir
                if self[new_node] in ' 1234':
                    weight = current_weight + 1
                    if new_node not in visited or weight < visited[new_node]:
                        visited[new_node] = weight
                        path[new_node] = min_node

        return visited, path

class MoveEnum(IntEnum):
    Null = 0
    Pass = 1
    Attack = 2
    Move = 3
    Push = 4

class Move:
    def __init__(self, move=MoveEnum.Null, direction=Point(0, 0), distance=0):
        self.move = move
        self.dir = direction
        self.dis = distance

    def __repr__(self):
        if self.move == MoveEnum.Null:
            return "NULL"
        elif self.move == MoveEnum.Pass:
            return "PASS"
        elif self.move == MoveEnum.Attack:
            return "ATTACK " + "NESW"[dirs.index(self.dir)]
        elif self.move == MoveEnum.Move:
            return "MOVE " + "NESW"[dirs.index(self.dir)] + " " + str(self.dis)
        elif self.move == MoveEnum.Push:
            return "PUSH " + "NESW"[dirs.index(self.dir)] + " " + str(self.dis)

arena = Arena(map_lines)
arena.me = MyBot(Point(0, 0), '@', 0, ep)

for line in stats_data:
    if line[0] == '@':
        arena.me.hp = int(line[2:-2])
    else:
        arena.bots.append(Bot(Point(0, 0), line[0], int(line[2:-2])))

arena.set_bots_locs()

current_danger = arena.danger(arena.me.pos)

moves = [] # format (move, damage done, danger, energy)

if arena.me.ep == 0:
    print(Move(MoveEnum.Pass))
    exit()

for bot, direction in arena.adjacent_bots_and_dirs():
    # Push to damage
    pushable_to = arena.look(arena.me.pos, direction,
                             arena.me.ep + 1, ignore=bot.ch)
    if '!' in pushable_to:
        distance = pushable_to.index('!')
        danger = arena.danger(arena.me.pos + (direction * distance), bot)
        danger -= current_danger
        moves.append((Move(MoveEnum.Push, direction, distance),
                      bot.hp, danger, distance))

    # Push to escape
    pushable_to = arena.look(arena.me.pos, direction,
                             arena.me.ep + 1, ignore=bot.ch, stopchars='#1234!')
    for distance in range(1, len(pushable_to)):
        danger = arena.danger(arena.me.pos + (direction * distance), bot)
        danger += bot.hp
        danger -= current_danger
        moves.append((Move(MoveEnum.Push, direction, distance),
                      0, danger, distance))

    # Attack
    bot.hp -= 1
    danger = arena.danger(arena.me.pos) - current_danger
    moves.append((Move(MoveEnum.Attack, direction), 1, danger, 1))
    bot.hp += 1

culled_moves = []

for move in moves: # Cull out attacks and pushes that result in certain death
    if current_danger + move[2] < arena.me.hp:
        culled_moves.append(move)

if len(culled_moves) == 1:
    print(culled_moves[0][0])
    exit()
elif len(culled_moves) > 1:
    best_move = culled_moves[0]

    for move in culled_moves:
        if move[1] - move[2] > best_move[1] - best_move[2]:
            best_move = move
        if move[1] - move[2] == best_move[1] - best_move[2] and move[3] < best_move[3]:
            best_move = move

    print (best_move[0])
    exit()

# Can't attack or push without dying, time to move

moves = []

if current_danger > 0: # Try to escape
    for direction in dirs:
        moveable_to = arena.moveable(arena.me.pos, direction, ep)

        i = 1

        for space in moveable_to:
            danger = arena.danger(arena.me.pos + (direction * i))
            danger -= current_danger
            moves.append((Move(MoveEnum.Move, direction, i), 0, danger, i))
            i += 1

    if len(moves) == 0: # Trapped and in mortal danger, attack biggest guy
        adjacents = arena.adjacent_bots()
        biggest = adjacents[0]
        for bot in adjacents:
            if biggest.hp < bot.hp:
                biggest = bot
        print (Move(MoveEnum.Attack, biggest.pos - arena.me.pos))
        exit()

    best_move = moves[0]
    for move in moves:
        if ((move[2] < best_move[2] and best_move[2] >= arena.me.hp) or
            (move[2] == best_move[2] and move[3] < best_move[3])):
            best_move = move

    print(best_move[0])
    exit()

else: # Seek out closest target with lower health
    distances, path = arena.path(arena.me.pos)

    bot_dists = list((bot, distances[bot.pos]) for bot in arena.bots)

    bot_dists.sort(key=lambda x: x[1])

    target = bot_dists[0]

    for i in range(len(bot_dists)):
        if bot_dists[i][0].hp <= arena.me.hp:
            target = bot_dists[i]
            break

    pos = target[0].pos
    for i in range(target[1] - 1):
        pos = path[pos]

    print (Move(MoveEnum.Move, pos - arena.me.pos, 1))
    exit()

# Shouldn't get here, but I might as well do something
print (Move(MoveEnum.Pass))
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ what version of python did you write this in? \$\endgroup\$ – Rip Leeb Nov 6 '14 at 19:42
  • \$\begingroup\$ @Nate 3.4.1 on win32 \$\endgroup\$ – user31611 Nov 6 '14 at 20:52
  • \$\begingroup\$ Thanks for submitting this one @horns. It was really fun to watch! \$\endgroup\$ – Rip Leeb Nov 9 '14 at 3:27
1
\$\begingroup\$

Gotta Finish Eating

Python:

import sys
print 'PASS'
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ I lol'd at that — and why the hell import sys? \$\endgroup\$ – tomsmeding Nov 7 '14 at 15:47
  • 1
    \$\begingroup\$ @tomsmeding Well I had to copy some stuff. And I thought in case I ever need to read some args :) when I finish eating, of course. \$\endgroup\$ – Timtech Nov 7 '14 at 22:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.