--- /dev/null
+#!/usr/bin/python -u
+
+#NOTE: The -u option is required for unbuffered stdin/stdout.
+# If stdin/stdout are buffered, the manager program will not recieve any messages and assume that the agent has timed out.
+
+
+import sys
+import random
+
+ranks = ['B','1','2','3','4','5','6','7','8','9','s','F', '?', '!', '+']
+
+"""
+The scaretable lists how `scary' pieces are to each other; pieces will move
+in the least scary direction.
+"""
+
+# B 1 2 3 4 5 6 7 8 9 s F ? ! +
+scaretable = [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #B
+ [ 0, 0,-8,-8,-7,-6,-5,-4,-3,-2, 5,-9, 0,-7, 0], #1
+ [ 0, 4, 0,-7,-6,-5,-4,-3,-2,-1,-2,-9,-3,-6, 0], #2
+ [ 0, 4, 2, 0,-6,-5,-4,-3,-2,-1,-2,-9,-2,-5, 0], #3
+ [ 0, 3, 2, 2, 0,-5,-4,-3,-2,-1,-2,-9,-1,-3, 0], #4
+ [ 0, 3, 2, 2, 2, 0,-4,-3,-2,-1,-2,-9, 0,-2, 0], #5
+ [ 0, 3, 2, 2, 2, 2, 0,-3,-2,-1,-2,-9, 1,-1, 0], #6
+ [ 0, 3, 2, 2, 2, 2, 2, 0,-2,-1,-2,-9,-1, 0, 0], #7
+ [-40, 3, 2, 2, 2, 2, 2, 2, 0,-2,-2,-9,-1, 1, 0], #8
+ [ 0, 3, 2, 2, 2, 2, 2, 2, 2, 0,-2,-9,-2, 2, 0], #9
+ [ 0, -5, 3, 3, 3, 3, 3, 3, 3, 3,-1,-9, 5, 3, 0], #s
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #F
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #?
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #!
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] #+
+
+"""
+The override table allows moves to be forced or prevented, thus ensuring
+that sacrifices are not made.
+"""
+# B 1 2 3 4 5 6 7 8 9 s F ? ! +
+overrides = [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #B
+ [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,-1,-1, 0, 0, 1], #1
+ [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,-1, 0, 0, 1], #2
+ [ 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,-1, 0, 0, 1], #3
+ [ 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,-1, 0, 0, 1], #4
+ [ 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,-1, 0, 0, 1], #5
+ [ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,-1, 0, 0, 1], #6
+ [ 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,-1, 0, 0, 1], #7
+ [-1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,-1, 0, 0, 1], #8
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,-1, 0, 0, 1], #9
+ [ 1,-1, 1, 1, 1, 1, 1, 1, 1, 1,-1,-1, 0, 0, 1], #s
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #F
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #?
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #!
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] #+
+
+
+def is_integer(s):
+ """ Using exceptions for this feels... wrong..."""
+ try:
+ int(s)
+ return True
+ except ValueError:
+ return False
+
+def move(x, y, direction, multiplier):
+ """ Moves point (x,y) in direction, returns a pair """
+ if direction == "UP":
+ return (x,y-multiplier)
+ elif direction == "DOWN":
+ return (x,y+multiplier)
+ elif direction == "LEFT":
+ return (x-multiplier, y)
+ elif direction == "RIGHT":
+ return (x+multiplier, y)
+ return (x,y)
+
+
+
+def oppositeColour(colour):
+ """ Returns the opposite colour to that given """
+ if colour == "RED":
+ return "BLUE"
+ elif colour == "BLUE":
+ return "RED"
+ else:
+ return "NONE"
+
+class Piece:
+ """ Class representing a piece
+ Pieces have colour, rank and co-ordinates
+ """
+ def __init__(self, colour, rank, x, y):
+ self.colour = colour
+ self.rank = rank
+ self.x = x
+ self.y = y
+ self.lastMoved = -1
+ self.beenRevealed = False
+ self.positions = [(x, y)]
+
+
+
+ self.heatmap = []
+ self.turnCount = 0
+
+ def mobile(self):
+ return self.rank != 'F' and self.rank != 'B' and self.rank != '?' and self.rank != '+'
+
+ def valuedRank(self):
+ if ranks.count(self.rank) > 0:
+ return len(ranks) - 2 - ranks.index(self.rank)
+ else:
+ return 0
+
+ def scariness(self, other):
+ scare = scaretable[ranks.index(self.rank)][ranks.index(other.rank)]
+ if scare > 0:
+ scare = scare * 1
+ return scare
+
+ def getOverride(self, other):
+ return overrides[ranks.index(self.rank)][ranks.index(other.rank)]
+
+ def getHeatmap(self, x,y,w,h):
+ if (x < 0) or (x >= w) or (y < 0) or (y >= h):
+ return 10
+ else:
+ return self.heatmap[x][y]
+
+ def validSquare(self, x, y, width, height, board):
+ if x < 0:
+ return False
+ if y < 0:
+ return False
+ if x >= width:
+ return False
+ if y >= height:
+ return False
+ if board[x][y] != None and board[x][y].colour == self.colour:
+ return False
+ if board[x][y] != None and board[x][y].rank == '#':
+ return False
+ return True
+
+ def generateHeatmap(self, width, height, board):
+ self.heatmap = []
+ newmap = []
+ for x in range(0,width):
+ self.heatmap.append([])
+ newmap.append([])
+ for y in range(0,height):
+ self.heatmap[x].append(0)
+ newmap[x].append(0)
+ if board[x][y] == None:
+ self.heatmap[x][y] = 0
+ continue
+ if board[x][y].colour == self.colour:
+ if board[x][y].rank == 'F':
+ self.heatmap[x][y] = -5 # + self.valuedRank() # Defend our flag
+ else:
+ self.heatmap[x][y] = self.scariness(board[x][y])
+
+ # Make pieces prefer to stay where they are
+ #self.heatmap[self.x][self.y] = -0.5
+
+ for i in range(0,min(30,len(self.positions))):
+ p = self.positions[len(self.positions)-1-i]
+ if board[p[0]][p[1]] != None:
+ self.heatmap[p[0]][p[1]] += 0.2 * ((50 - i)/50)
+
+
+
+ for n in range(0,8):
+ for x in range(0,width):
+ for y in range(0,height):
+ if self.heatmap[x][y] != 0:
+ newmap[x][y] = self.heatmap[x][y]
+ continue
+ newmap[x][y] = 0 #self.heatmap[x][y] * 0.2
+ if self.validSquare(x-1,y,width,height,board):
+ newmap[x][y] += self.heatmap[x-1][y] * 0.2
+ else:
+ newmap[x][y] += 0 #self.heatmap[x][y] * 0.1
+ if self.validSquare(x+1,y,width,height,board):
+ newmap[x][y] += self.heatmap[x+1][y] * 0.2
+ else:
+ newmap[x][y] += 0 #self.heatmap[x][y] * 0.1
+ if self.validSquare(x,y-1,width,height,board):
+ newmap[x][y] += self.heatmap[x][y-1] * 0.2
+ else:
+ newmap[x][y] += 0 #self.heatmap[x][y] * 0.1
+ if self.validSquare(x,y+1,width,height,board):
+ newmap[x][y] += self.heatmap[x][y+1] * 0.2
+ else:
+ newmap[x][y] += 0 #self.heatmap[x][y] * 0.1
+ self.heatmap = newmap
+
+ def debugPrintHeat(self,w,h):
+ """ For debug purposes only. Prints the board to stderr.
+ Does not indicate difference between allied and enemy pieces
+ Unknown (enemy) pieces are shown as '?'
+ """
+ sys.stderr.write("Pos: " + str(self.x) + ", " + str(self.y) + " -- rank: " + str(self.rank) + "\n")
+ for y in range(0, h):
+ for x in range(0, w):
+ if (self.heatmap[x][y] - self.heatmap[self.x][self.y] > 0.0):
+ sys.stderr.write("O")
+ elif (self.heatmap[x][y] - self.heatmap[self.x][self.y] == 0.0):
+ sys.stderr.write("X")
+ elif (self.heatmap[x][y] - self.heatmap[self.x][self.y] < 0.0):
+ sys.stderr.write(".")
+ else:
+ sys.stderr.write(" ")
+ sys.stderr.write("\n")
+ sys.stderr.write("\n")
+
+
+
+
+def valuedRank(rank):
+ if ranks.count(rank) > 0:
+ return len(ranks) - 2 - ranks.index(rank)
+ else:
+ return 0
+
+
+
+class SulixAI:
+ """
+ BasicAI class to play a game of stratego
+ Implements the protocol correctly. Stores the state of the board in self.board
+ Only makes random moves.
+ Override method "MakeMove" for more complex moves
+ """
+ def __init__(self):
+ """ Constructs the BasicAI agent, and starts it playing the game """
+ #sys.stderr.write("BasicAI __init__ here...\n");
+ self.turn = 0
+ self.board = []
+ self.units = []
+ self.enemyUnits = []
+
+ self.total_turns = 0
+
+ self.totalAllies = {'B':6,'1':1,'2':1,'3':2,'4':3,'5':4,'6':4,'7':4,'8':5,'9':8,'s':1,'F':1}
+ self.totalEnemies = {'B':6,'1':1,'2':1,'3':2,'4':3,'5':4,'6':4,'7':4,'8':5,'9':8,'s':1,'F':1}
+ self.hiddenEnemies = {'B':6,'1':1,'2':1,'3':2,'4':3,'5':4,'6':4,'7':4,'8':5,'9':8,'s':1,'F':1}
+ self.hiddenAllies = {'B':6,'1':1,'2':1,'3':2,'4':3,'5':4,'6':4,'7':4,'8':5,'9':8,'s':1,'F':1}
+ self.numEnemies = 6+1+1+2+3+4+4+4+5+8+1+1
+ self.numStillEnemies = 6+1
+ self.lastMoved = None
+
+
+
+ def Setup(self):
+ """ Implements Setup part of protocol. Always uses the same setup. Override to create custom setups """
+ #sys.stderr.write("BasicAI Setup here...\n");
+ setup = sys.stdin.readline().split(' ')
+ if len(setup) != 4:
+ sys.stderr.write("BasicAI setup fails, expected 4 tokens, got " + str(len(setup)) + " "+str(setup) + "\n")
+ self.colour = setup[0]
+ self.opponentName = setup[1]
+ self.width = int(setup[2])
+ self.height = int(setup[3])
+ for x in range(0, self.width):
+ self.board.append([])
+ for y in range(0, self.height):
+ self.board[x].append(None)
+ if self.colour == "RED":
+ print "FB8sB979B8\nBB99555583\n6724898974\nB314676699"
+ elif self.colour == "BLUE":
+ print "B314676699\n6724898974\nBB99555583\nFB8sB979B8"
+ return True
+
+ def MoveCycle(self):
+ #sys.stderr.write("BasicAI MakeMove here...\n");
+ if self.InterpretResult() == False or self.ReadBoard() == False or self.MakeMove() == False:
+ return False
+ self.turn += 1
+ return self.InterpretResult()
+
+ def MakeMove(self):
+ """ Randomly moves any moveable piece, or prints "NO_MOVE" if there are none """
+ #TODO: Over-ride this function in base classes with more complex move behaviour
+
+ #sys.stderr.write("Sulix's AI makes a move...\n")
+ #self.debugPrintBoard()
+
+ if len(self.units) <= 0:
+ return False
+
+ index = random.randint(0, len(self.units)-1)
+ startIndex = index
+
+ directions = ("UP", "DOWN", "LEFT", "RIGHT")
+ bestdir = 0
+ bestScare = 999
+ bestpiece = None
+ while True:
+ piece = self.units[index]
+
+ if piece != None and piece.mobile():
+ dirIndex = random.randint(0, len(directions)-1)
+ startDirIndex = dirIndex
+ piece.generateHeatmap(self.width, self.height, self.board)
+ currentScary = piece.getHeatmap(piece.x, piece.y, self.width, self.height) * 0 + piece.turnCount*0 #Perhaps just look for the best move
+ piece.turnCount = piece.turnCount + 1
+ while True:
+ #sys.stderr.write("Trying index " + str(dirIndex) + "\n")
+ p = move(piece.x, piece.y, directions[dirIndex],1)
+ if p[0] >= 0 and p[0] < self.width and p[1] >= 0 and p[1] < self.height:
+ target = self.board[p[0]][p[1]]
+ if target == None or (target.colour != piece.colour and target.colour != "NONE" and target.colour != "BOTH"):
+ scare = piece.getHeatmap(p[0], p[1],self.width, self.height) - currentScary
+ override = 0
+ if target != None:
+ override = piece.getOverride(target)
+
+ if (self.total_turns % 250 < 15) and (self.total_turns > 250):
+ scare += random.randint(0, 5)
+
+
+ if override == 1:
+ scare = 998
+ elif override == -1:
+ piece.turnCount = 0
+ print str(piece.x) + " " + str(piece.y) + " " + directions[dirIndex]
+ return True
+
+
+
+
+ if scare < bestScare:
+ bestdir = dirIndex
+ bestScare = scare
+ bestpiece = piece
+
+ dirIndex = (dirIndex + 1) % len(directions)
+ if startDirIndex == dirIndex:
+ break
+
+
+ index = (index + 1) % len(self.units)
+ if startIndex == index:
+ if bestScare != 999:
+ bestpiece.turnCount = 0
+ print str(bestpiece.x) + " " + str(bestpiece.y) + " "+directions[bestdir]
+# bestpiece.debugPrintHeat(self.width, self.height)
+ return True
+ else:
+ print "SURRENDER"
+ return True
+
+
+ def ReadBoard(self):
+ """ Reads in the board.
+ On the very first turn, sets up the self.board structure
+ On subsequent turns, the board is simply read, but the self.board structure is not updated here.
+ """
+ #sys.stderr.write("BasicAI ReadBoard here...\n");
+ for y in range(0,self.height):
+ row = sys.stdin.readline().strip()
+ if len(row) < self.width:
+ sys.stderr.write("Row has length " + str(len(row)) + " vs " + str(self.width) + "\n")
+ return False
+ for x in range(0,self.width):
+ if self.turn == 0:
+ if row[x] == '.':
+ pass
+ elif row[x] == '#':
+ self.board[x][y] = Piece(oppositeColour(self.colour), '?',x,y)
+ self.enemyUnits.append(self.board[x][y])
+ elif row[x] == '+':
+ self.board[x][y] = Piece("NONE", '+', x, y)
+ else:
+ self.board[x][y] = Piece(self.colour, row[x],x,y)
+ self.units.append(self.board[x][y])
+ else:
+ pass
+ return True
+
+
+ def InterpretResult(self):
+ """ Interprets the result of a move, and updates the board.
+ The very first move is ignored.
+ On subsequent moves, the self.board structure is updated
+ """
+
+ self.total_turns = self.total_turns + 1
+
+ #sys.stderr.write("BasicAI InterpretResult here...\n")
+ result = sys.stdin.readline().split(' ')
+ #sys.stderr.write(" Read status line \"" + str(result) + "\"\n")
+ if self.turn == 0:
+ return True
+
+ if result[0].strip() == "QUIT": #Make sure we exit when the manager tells us to!
+ return False
+
+ if result[0].strip() == "NO_MOVE": #No move was made, don't need to update anything
+ return True
+
+ if len(result) < 4: #Should be at least 4 tokens (X Y DIRECTION OUTCOME) in any other case
+ return False
+
+ x = int(result[0].strip())
+ y = int(result[1].strip())
+
+
+ # The piece moved! It's not a bomb
+ if self.board[x][y].rank == '?':
+ self.board[x][y].rank = '!'
+ #sys.stderr.write(" Board position " + str(x) + " " + str(y) + " is OK!\n")
+
+ direction = result[2].strip()
+
+ multiplier = 1
+ outcome = result[3].strip()
+ outIndex = 3
+ if is_integer(outcome):
+ multiplier = int(outcome)
+ outcome = result[4].strip()
+ outIndex = 4
+
+ p = move(x,y,direction, multiplier)
+
+ # It's a scout! I saw it move.
+ if multiplier > 1:
+ self.board[x][y].rank = '9'
+
+ #Determine attacking piece
+ attacker = self.board[x][y]
+ self.board[x][y] = None
+
+ if attacker == None:
+ return False
+
+ lastMoved = attacker
+
+ defender = self.board[p[0]][p[1]]
+
+ #Update attacker's position (Don't overwrite the board yet though)
+
+ attacker.x = p[0]
+ attacker.y = p[1]
+ attacker.positions.insert(0, (attacker.x, attacker.y))
+
+
+ #Determine ranks of pieces if supplied
+ if len(result) >= outIndex + 3:
+ if defender == None:
+ return False
+ attacker.rank = result[outIndex+1].strip()
+ if attacker.beenRevealed == False:
+ if attacker.colour == self.colour:
+ self.hiddenAllies[attacker.rank] -= 1
+ elif attacker.colour == oppositeColour(self.colour):
+ self.hiddenEnemies[attacker.rank] -= 1
+ attacker.beenRevealed = True
+ defender.rank = result[outIndex+2].strip()
+ if defender.beenRevealed == False:
+ if defender.colour == self.colour:
+ self.hiddenAllies[defender.rank] -= 1
+ elif defender.colour == oppositeColour(self.colour):
+ self.hiddenEnemies[defender.rank] -= 1
+
+ defender.beenRevealed = True
+
+
+
+ if outcome == "OK":
+ self.board[p[0]][p[1]] = attacker
+
+ elif outcome == "KILLS":
+ self.board[p[0]][p[1]] = attacker
+
+ if defender.colour == self.colour:
+ self.totalAllies[defender.rank] -= 1
+ self.units.remove(defender)
+ elif defender.colour == oppositeColour(self.colour):
+ self.totalEnemies[defender.rank] -= 1
+ self.enemyUnits.remove(defender)
+
+ elif outcome == "DIES":
+ if attacker.colour == self.colour:
+ self.totalAllies[attacker.rank] -= 1
+ self.units.remove(attacker)
+ elif attacker.colour == oppositeColour(self.colour):
+ self.totalEnemies[attacker.rank] -= 1
+ self.enemyUnits.remove(attacker)
+ if attacker.rank == 'B':
+ self.numStillEnemies -= 1
+ #if self.numStillEnemies == 0: # There are no bombs left
+ for i in range(0,ranks['s']):
+ scaretable[i][ranks['?']] -= 5
+
+ elif outcome == "BOTHDIE":
+ self.board[p[0]][p[1]] = None
+
+ if defender.colour == self.colour:
+ self.totalAllies[defender.rank] -= 1
+ self.units.remove(defender)
+ elif defender.colour == oppositeColour(self.colour):
+ self.totalEnemies[defender.rank] -= 1
+ self.enemyUnits.remove(defender)
+
+ if attacker.colour == self.colour:
+ self.totalAllies[attacker.rank] -= 1
+ self.units.remove(attacker)
+ elif attacker.colour == oppositeColour(self.colour):
+ self.totalEnemies[attacker.rank] -= 1
+ self.enemyUnits.remove(attacker)
+ elif outcome == "FLAG":
+ #sys.stderr.write(" Game over!\n")
+ return False
+ elif outcome == "ILLEGAL":
+ #sys.stderr.write(" Illegal move!\n")
+ return False
+ else:
+ #sys.stderr.write(" Don't understand outcome \"" + outcome + "\"!\n");
+ return False
+
+
+
+
+ #sys.stderr.write(" Completed interpreting move!\n");
+ return True
+
+
+
+ def debugPrintBoard(self):
+ """ For debug purposes only. Prints the board to stderr.
+ Does not indicate difference between allied and enemy pieces
+ Unknown (enemy) pieces are shown as '?'
+ """
+ for y in range(0, self.height):
+ for x in range(0, self.width):
+ if self.board[x][y] == None:
+ sys.stderr.write(".");
+ else:
+ sys.stderr.write(str(self.board[x][y].rank));
+ sys.stderr.write("\n")
+
+if __name__ == "__main__":
+ sulixAI = SulixAI()
+ if sulixAI.Setup():
+ while sulixAI.MoveCycle():
+ pass
+