Scriptbot Warz!

14

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.

Rip Leeb

Posted 2014-10-30T20:36:39.053

Reputation: 1 250

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 – 2014-10-30T20:49:25.983

@BrainSteel Yes the map will always be bounded by # and your Scriptbot will start inside of these bounds. – Rip Leeb – 2014-10-30T20:50:39.973

3

If you're sure you've missed something, why not post it in the sandbox?

– Martin Ender – 2014-10-30T20:53:02.140

If there is no bot present in the direction specified in a PUSH command, how does the command function? – BrainSteel – 2014-10-30T20:53:29.500

2@MartinBüttner I've thought this through quite thoroughly. I was just trying to be friendly and make it clear that questions are welcome. – Rip Leeb – 2014-10-30T20:57:57.417

@BrainSteel If the PUSH is invalid, the GameMaster script will do nothing, zero EP will be spent, and the Scriptbot will be called again. – Rip Leeb – 2014-10-30T21:07:26.303

I assume EP is 1 per DISTANCE for MOVE/PUSH, but what's the EP cost for ATTACK? You say the program will be invoked a maximum of 10 times, but what if I start the turn with 11 EP (from being at 10 HP)? – Geobits – 2014-10-30T23:02:16.310

>

  • Do unused EP accumulate? 2. What is meant by The map might take any form ?
  • < – feersum – 2014-10-30T23:07:49.993

    Will a PASS option be added, or must I use all my EP each turn on real actions? – Geobits – 2014-10-30T23:32:53.307

    @Geobits ATTACK is 1 EP. I have clarified this in the list of actions. I have also changed the max calls to 11. I have added PASS as an option. I also tried to add a visual bracket for clarification for the tournament style. Thank you! @ferrsum No EP does not accumulate. I have added that to the Q&A. – Rip Leeb – 2014-10-30T23:44:26.697

    @Geobits As in a typical bracket, a Scriptbot will get one entry into the tournament and when they're out, they're out. – Rip Leeb – 2014-10-30T23:54:13.647

    I guess the only question I have left about the bracket is: Assuming the number of entries isn't a perfect power of 4, how are the blanks handled? Will you add a dummy bot to fill them, use a bye system, or something else? – Geobits – 2014-10-31T00:02:44.513

    @Geobits There will be no dummy bots. I will try to make the bracket as fair as possible with the submissions that I am given. I have updated the Submission and Judging section with more info. Thanks! – Rip Leeb – 2014-10-31T12:26:15.830

    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/state file will only be valid prior to the first action. – COTO – 2014-10-31T14:01:56.817

    Shouldn't the ranking be most HP, most damage dealt, least damage taken? Surely a bot that takes less damage is better than one that takes more.

    Also will you release the GameMaster script for testing? – IchBinKeinBaum – 2014-10-31T14:53:39.350

    @COTO I've answered in the Q&A section and also clarified the main document. – Rip Leeb – 2014-10-31T15:19:00.043

    @IchBinKeinBaum I think that point can be argued. I would say that taking more damage indicates that your battle was tougher and so you deserve to win. I hope that it will not come to that level of detail. If you can think of a better third rank criteria, I am willing to replace damage taken. The GameMaster script will not be released. I would encourage you to create a map and stats file to test your bot's behavior. – Rip Leeb – 2014-10-31T15:19:44.570

    Does the rule above During a turn, the Scriptbot is invoked repeatedly until their EP is spent, or a maximum of 11 times mean that if robotA is next to robotB, it can attack 10 (or 11) times and kill robotB? Put another way, if I have 5 health points left and I leave my robot within 5 spaces of another robot, that robot can move and kill my robot in one turn, before I could act? – Logic Knight – 2014-10-31T16:07:11.017

    @Mike yes, that's right. – Rip Leeb – 2014-10-31T16:46:38.303

    For our own testing in movement and such, will you/could you provide a handful of maps that may be used in the simulation? – BrainSteel – 2014-10-31T16:50:39.273

    1If robotA pushes robotB into a trap, is robotA credited with "damage dealt" points equal to the current health of robotB? – Logic Knight – 2014-10-31T17:34:07.043

    @BrainSteel Sure, I will work on a few maps. Look for them in the main document soon. – Rip Leeb – 2014-10-31T17:36:54.963

    @MikeSweeney; I think you have the same idea I do, lol. I was going to code "It's a tarp" bot (or maybe, a homage to Soul Asylum "I want somebody to shove"), and just push everyone into traps, lol. – DreamWarrior – 2014-10-31T17:37:09.323

    @MikeSweeney 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. – Rip Leeb – 2014-10-31T17:38:32.867

    Also, I think you should ensure your random starting points are at least subject to a criteria that no bot is within immediate death distance of other bot(s). In other words, it's not really fair if bot A starts close enough to a set of bots (B, C, etc) who can move close to it and attack it to death before bot A even has a chance to execute. Or, at the very least, configure the bracket system in such a way that all bots execute at least once before they are eliminated.... – DreamWarrior – 2014-10-31T17:42:16.850

    @DreamWarrior I should clarify. The order is random. The starting position will be predefined so that no one bot can kill another bot on their first turn. – Rip Leeb – 2014-10-31T17:49:27.293

    Answers

    1

    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;
            }
        }
    }
    

    CommonGuy

    Posted 2014-10-30T20:36:39.053

    Reputation: 4 684

    What version of Java was this written in? – Rip Leeb – 2014-11-07T16:26:42.793

    @Nate I used 1.7. – CommonGuy – 2014-11-07T16:33:34.103

    3

    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?
    

    Logic Knight

    Posted 2014-10-30T20:36:39.053

    Reputation: 6 622

    elif ch in '123': You want to look for at least 1234. If you are bot 3, the opponent list would be 124. – Rip Leeb – 2014-10-31T18:27:19.853

    1@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. – Logic Knight – 2014-11-01T04:48:16.113

    3

    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))
    

    user31611

    Posted 2014-10-30T20:36:39.053

    Reputation:

    what version of python did you write this in? – Rip Leeb – 2014-11-06T19:42:46.170

    @Nate 3.4.1 on win32 – None – 2014-11-06T20:52:22.880

    Thanks for submitting this one @horns. It was really fun to watch! – Rip Leeb – 2014-11-09T03:27:49.383

    1

    Gotta Finish Eating

    Python:

    import sys
    print 'PASS'
    

    Timtech

    Posted 2014-10-30T20:36:39.053

    Reputation: 12 038

    1I lol'd at that — and why the hell import sys? – tomsmeding – 2014-11-07T15:47:32.527

    1@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. – Timtech – 2014-11-07T22:34:00.030