├── AmbiturnerBot.py ├── DiscerningBot.py ├── ImprovedBot.py ├── MyBot.py ├── OverkillBot.py ├── PatientBot.py ├── ProductionBot.py ├── README.md ├── RandomBot.py ├── hlt.py ├── runGame.bat └── runGame.sh /AmbiturnerBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("AmbiturnerBot") 8 | 9 | 10 | def find_nearest_enemy_direction(square): 11 | direction = NORTH 12 | max_distance = min(game_map.width, game_map.height) / 2 13 | for d in (NORTH, EAST, SOUTH, WEST): 14 | distance = 0 15 | current = square 16 | while current.owner == myID and distance < max_distance: 17 | distance += 1 18 | current = game_map.get_target(current, d) 19 | if distance < max_distance: 20 | direction = d 21 | max_distance = distance 22 | return direction 23 | 24 | def get_move(square): 25 | _, direction = next(((neighbor.strength, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) if neighbor.owner != myID and neighbor.strength < square.strength), (None, None)) 26 | if direction is not None: 27 | return Move(square, direction) 28 | elif square.strength < square.production * 5: 29 | return Move(square, STILL) 30 | 31 | border = any(neighbor.owner != myID for neighbor in game_map.neighbors(square)) 32 | if not border: 33 | return Move(square, find_nearest_enemy_direction(square)) 34 | else: 35 | #wait until we are strong enough to attack 36 | return Move(square, STILL) 37 | 38 | 39 | while True: 40 | game_map.get_frame() 41 | moves = [get_move(square) for square in game_map if square.owner == myID] 42 | hlt.send_frame(moves) 43 | 44 | -------------------------------------------------------------------------------- /DiscerningBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("DiscerningBot") 8 | 9 | 10 | def find_nearest_enemy_direction(square): 11 | direction = NORTH 12 | max_distance = min(game_map.width, game_map.height) / 2 13 | for d in (NORTH, EAST, SOUTH, WEST): 14 | distance = 0 15 | current = square 16 | while current.owner == myID and distance < max_distance: 17 | distance += 1 18 | current = game_map.get_target(current, d) 19 | if distance < max_distance: 20 | direction = d 21 | max_distance = distance 22 | return direction 23 | 24 | def heuristic(square): 25 | return square.production / square.strength if square.strength else square.production 26 | 27 | def get_move(square): 28 | target, direction = max(((neighbor, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) 29 | if neighbor.owner != myID), 30 | default = (None, None), 31 | key = lambda t: heuristic(t[0])) 32 | if target is not None and target.strength < square.strength: 33 | return Move(square, direction) 34 | elif square.strength < square.production * 5: 35 | return Move(square, STILL) 36 | 37 | border = any(neighbor.owner != myID for neighbor in game_map.neighbors(square)) 38 | if not border: 39 | return Move(square, find_nearest_enemy_direction(square)) 40 | else: 41 | #wait until we are strong enough to attack 42 | return Move(square, STILL) 43 | 44 | 45 | while True: 46 | game_map.get_frame() 47 | moves = [get_move(square) for square in game_map if square.owner == myID] 48 | hlt.send_frame(moves) 49 | 50 | -------------------------------------------------------------------------------- /ImprovedBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("ImprovedBot") 8 | 9 | 10 | def get_move(square): 11 | _, direction = next(((neighbor.strength, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) if neighbor.owner != myID and neighbor.strength < square.strength), (None, None)) 12 | if direction is not None: 13 | return Move(square, direction) 14 | elif square.strength < square.production * 5: 15 | return Move(square, STILL) 16 | else: 17 | return Move(square, NORTH if random.random() > 0.5 else WEST) 18 | 19 | 20 | while True: 21 | game_map.get_frame() 22 | moves = [get_move(square) for square in game_map if square.owner == myID] 23 | hlt.send_frame(moves) 24 | 25 | -------------------------------------------------------------------------------- /MyBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("MyPythonBot") 8 | 9 | while True: 10 | game_map.get_frame() 11 | moves = [Move(square, random.choice((NORTH, EAST, SOUTH, WEST, STILL))) for square in game_map if square.owner == myID] 12 | hlt.send_frame(moves) 13 | -------------------------------------------------------------------------------- /OverkillBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("OverkillBot") 8 | 9 | 10 | def find_nearest_enemy_direction(square): 11 | direction = NORTH 12 | max_distance = min(game_map.width, game_map.height) / 2 13 | for d in (NORTH, EAST, SOUTH, WEST): 14 | distance = 0 15 | current = square 16 | while current.owner == myID and distance < max_distance: 17 | distance += 1 18 | current = game_map.get_target(current, d) 19 | if distance < max_distance: 20 | direction = d 21 | max_distance = distance 22 | return direction 23 | 24 | def heuristic(square): 25 | if square.owner == 0 and square.strength > 0: 26 | return square.production / square.strength 27 | else: 28 | # return total potential damage caused by overkill when attacking this square 29 | return sum(neighbor.strength for neighbor in game_map.neighbors(square) if neighbor.owner not in (0, myID)) 30 | 31 | def get_move(square): 32 | target, direction = max(((neighbor, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) 33 | if neighbor.owner != myID), 34 | default = (None, None), 35 | key = lambda t: heuristic(t[0])) 36 | if target is not None and target.strength < square.strength: 37 | return Move(square, direction) 38 | elif square.strength < square.production * 5: 39 | return Move(square, STILL) 40 | 41 | border = any(neighbor.owner != myID for neighbor in game_map.neighbors(square)) 42 | if not border: 43 | return Move(square, find_nearest_enemy_direction(square)) 44 | else: 45 | #wait until we are strong enough to attack 46 | return Move(square, STILL) 47 | 48 | 49 | while True: 50 | game_map.get_frame() 51 | moves = [get_move(square) for square in game_map if square.owner == myID] 52 | hlt.send_frame(moves) 53 | 54 | -------------------------------------------------------------------------------- /PatientBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("PatientBot") 8 | 9 | 10 | def get_move(square): 11 | _, direction = next(((neighbor.strength, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) if neighbor.owner != myID and neighbor.strength < square.strength), (None, None)) 12 | if direction is not None: 13 | return Move(square, direction) 14 | elif square.strength < square.production * 5: 15 | return Move(square, STILL) 16 | 17 | border = any(neighbor.owner != myID for neighbor in game_map.neighbors(square)) 18 | if not border: 19 | return Move(square, NORTH if random.random() > 0.5 else WEST) 20 | else: 21 | #wait until we are strong enough to attack 22 | return Move(square, STILL) 23 | 24 | 25 | while True: 26 | game_map.get_frame() 27 | moves = [get_move(square) for square in game_map if square.owner == myID] 28 | hlt.send_frame(moves) 29 | 30 | -------------------------------------------------------------------------------- /ProductionBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("ProductionBot") 8 | 9 | 10 | def find_nearest_enemy_direction(square): 11 | direction = NORTH 12 | max_distance = min(game_map.width, game_map.height) / 2 13 | for d in (NORTH, EAST, SOUTH, WEST): 14 | distance = 0 15 | current = square 16 | while current.owner == myID and distance < max_distance: 17 | distance += 1 18 | current = game_map.get_target(current, d) 19 | if distance < max_distance: 20 | direction = d 21 | max_distance = distance 22 | return direction 23 | 24 | def get_move(square): 25 | target, direction = max(((neighbor, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) 26 | if neighbor.owner != myID), 27 | default = (None, None), 28 | key = lambda t: t[0].production) 29 | if target is not None and target.strength < square.strength: 30 | return Move(square, direction) 31 | elif square.strength < square.production * 5: 32 | return Move(square, STILL) 33 | 34 | border = any(neighbor.owner != myID for neighbor in game_map.neighbors(square)) 35 | if not border: 36 | return Move(square, find_nearest_enemy_direction(square)) 37 | else: 38 | #wait until we are strong enough to attack 39 | return Move(square, STILL) 40 | 41 | 42 | while True: 43 | game_map.get_frame() 44 | moves = [get_move(square) for square in game_map if square.owner == myID] 45 | hlt.send_frame(moves) 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alt-python3-halite-starter 2 | Alternative Halite starter package for Python3 3 | 4 | The main contribution of this alt Halite starter package is a signficantly refactored hlt.py (which also subsumes the networking.py file). 5 | 6 | My initial bots were RandomBot, and ImprovedBot, just like most everyone else. After seeing how ImprovedBot worked, ideas immediately come, and it's natural to just extend that existing code. After a couple submits to the Halite servers, I then ran into two problems using the official hlt.py code: 7 | 8 | * It didn't seem pythonic, and I found it very difficult to work with. I constantly confused Sites and Locations, and all the double-looping in my botcode was killing its readability. 9 | 10 | * It was slow, and my simple bots were timing out. I think this was caused by all the indirect object references. 11 | 12 | The improved features and differences with the official starter package as it relates to hlt.py are these: 13 | 14 | * I eliminated Sites and Locations. These have been combined into just one object: a Square. Square is simply a namedtuple with five fields: x, y, owner, strength, production. 15 | 16 | * The game_map is iterable. Instead of writing: 17 | ```python 18 | # No, no, no, ugh 19 | for x in range(gameMap.width): 20 | for y in range(gameMap.height): 21 | square = gameMap.getSite(Location(x, y)) 22 | ``` 23 | 24 | We can just write: 25 | ```python 26 | for square in game_map: 27 | ``` 28 | 29 | and then nifty stuff like: 30 | ```python 31 | # I can't even bring myself to write the translation of this using the official hlt.py 32 | my_production = sum(square.production for square in game_map if square.owner == myID) 33 | ``` 34 | 35 | * I added a neighbors method for game_map. You can read how it works, but instead of writing gross code like this: 36 | ```python 37 | if any(getSite(my_loc, direction).owner != myID for direction in CARDINALS): 38 | # do something 39 | ``` 40 | 41 | It's a lot more readable (to my eye) this way: 42 | ```python 43 | if any(neighbor.owner != myID for neighbor in game_map.neighbors(square)): 44 | # do something 45 | ``` 46 | 47 | Sometimes we just want to iterate over the neighbor squares as above, and other times we also need to know which direction each neighbor is from our square. We can use Python's enumerate() function to accomplish this: 48 | 49 | ```python 50 | baddest_neighbor, direction = max((neighbor.strength, direction) for direction, neighbor in enumerate(game_map.neighbors(square)) if neighbor.owner != myID) 51 | ``` 52 | 53 | 54 | In this repo, I've included the same files as the official Halite starter package but with the botcode refactored to work with the refactored hlt.py. I've also included Python3 translations of the "Now what?" bots created by @nmalaguti in his excellent Halite forum post http://forums.halite.io/t/so-youve-improved-the-random-bot-now-what/482 Reviewing the code for these bots should highlight the functionality of the refactored hlt.py in this alt starter package. 55 | 56 | Official package RandomBot main logic loop: 57 | ```python3 58 | while True: 59 | moves = [] 60 | gameMap = getFrame() 61 | for y in range(gameMap.height): 62 | for x in range(gameMap.width): 63 | location = Location(x, y) 64 | if gameMap.getSite(location).owner == myID: 65 | moves.append(Move(location, random.choice(DIRECTIONS))) 66 | sendFrame(moves) 67 | ``` 68 | 69 | Alt package RandomBot main logic loop: 70 | ```python3 71 | while True: 72 | game_map.get_frame() 73 | moves = [Move(square, random.choice(DIRECTIONS)) for square in game_map if square.owner == myID] 74 | hlt.send_frame(moves) 75 | ``` 76 | 77 | After working with this refactored hlt.py, I find I can express my bot-code ideas much more quickly and expressively. And it's significantly faster. I'm getting speed-ups of anywhere 4x to 10x over same logic written with the official starter package, and timing out is a distant memory. 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /RandomBot.py: -------------------------------------------------------------------------------- 1 | import hlt 2 | from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square 3 | import random 4 | 5 | 6 | myID, game_map = hlt.get_init() 7 | hlt.send_init("RandomPythonBot") 8 | 9 | while True: 10 | game_map.get_frame() 11 | moves = [Move(square, random.choice((NORTH, EAST, SOUTH, WEST, STILL))) for square in game_map if square.owner == myID] 12 | hlt.send_frame(moves) 13 | -------------------------------------------------------------------------------- /hlt.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Python-based Halite starter-bot framework. 3 | 4 | This module contains a Pythonic implementation of a Halite starter-bot framework. 5 | In addition to a class (GameMap) containing all information about the game world 6 | and some helper methods, the module also imeplements the functions necessary for 7 | communicating with the Halite game environment. 8 | """ 9 | 10 | import sys 11 | from collections import namedtuple 12 | from itertools import chain, zip_longest 13 | 14 | 15 | def grouper(iterable, n, fillvalue=None): 16 | "Collect data into fixed-length chunks or blocks" 17 | # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" 18 | args = [iter(iterable)] * n 19 | return zip_longest(*args, fillvalue=fillvalue) 20 | 21 | # Because Python uses zero-based indexing, the cardinal directions have a different mapping in this Python starterbot 22 | # framework than that used by the Halite game environment. This simplifies code in several places. To accommodate 23 | # this difference, the translation to the indexing system used by the game environment is done automatically by 24 | # the send_frame function when communicating with the Halite game environment. 25 | 26 | NORTH, EAST, SOUTH, WEST, STILL = range(5) 27 | 28 | def opposite_cardinal(direction): 29 | "Returns the opposing cardinal direction." 30 | return (direction + 2) % 4 if direction != STILL else STILL 31 | 32 | 33 | Square = namedtuple('Square', 'x y owner strength production') 34 | 35 | 36 | Move = namedtuple('Move', 'square direction') 37 | 38 | 39 | class GameMap: 40 | def __init__(self, size_string, production_string, map_string=None): 41 | self.width, self.height = tuple(map(int, size_string.split())) 42 | self.production = tuple(tuple(map(int, substring)) for substring in grouper(production_string.split(), self.width)) 43 | self.contents = None 44 | self.get_frame(map_string) 45 | self.starting_player_count = len(set(square.owner for square in self)) - 1 46 | 47 | def get_frame(self, map_string=None): 48 | "Updates the map information from the latest frame provided by the Halite game environment." 49 | if map_string is None: 50 | map_string = get_string() 51 | split_string = map_string.split() 52 | owners = list() 53 | while len(owners) < self.width * self.height: 54 | counter = int(split_string.pop(0)) 55 | owner = int(split_string.pop(0)) 56 | owners.extend([owner] * counter) 57 | assert len(owners) == self.width * self.height 58 | assert len(split_string) == self.width * self.height 59 | self.contents = [[Square(x, y, owner, strength, production) 60 | for x, (owner, strength, production) 61 | in enumerate(zip(owner_row, strength_row, production_row))] 62 | for y, (owner_row, strength_row, production_row) 63 | in enumerate(zip(grouper(owners, self.width), 64 | grouper(map(int, split_string), self.width), 65 | self.production))] 66 | 67 | def __iter__(self): 68 | "Allows direct iteration over all squares in the GameMap instance." 69 | return chain.from_iterable(self.contents) 70 | 71 | def neighbors(self, square, n=1, include_self=False): 72 | "Iterable over the n-distance neighbors of a given square. For single-step neighbors, the enumeration index provides the direction associated with the neighbor." 73 | assert isinstance(include_self, bool) 74 | assert isinstance(n, int) and n > 0 75 | if n == 1: 76 | combos = ((0, -1), (1, 0), (0, 1), (-1, 0), (0, 0)) # NORTH, EAST, SOUTH, WEST, STILL ... matches indices provided by enumerate(game_map.neighbors(square)) 77 | else: 78 | combos = ((dx, dy) for dy in range(-n, n+1) for dx in range(-n, n+1) if abs(dx) + abs(dy) <= n) 79 | return (self.contents[(square.y + dy) % self.height][(square.x + dx) % self.width] for dx, dy in combos if include_self or dx or dy) 80 | 81 | def get_target(self, square, direction): 82 | "Returns a single, one-step neighbor in a given direction." 83 | dx, dy = ((0, -1), (1, 0), (0, 1), (-1, 0), (0, 0))[direction] 84 | return self.contents[(square.y + dy) % self.height][(square.x + dx) % self.width] 85 | 86 | def get_distance(self, sq1, sq2): 87 | "Returns Manhattan distance between two squares." 88 | dx = min(abs(sq1.x - sq2.x), sq1.x + self.width - sq2.x, sq2.x + self.width - sq1.x) 89 | dy = min(abs(sq1.y - sq2.y), sq1.y + self.height - sq2.y, sq2.y + self.height - sq1.y) 90 | return dx + dy 91 | 92 | ##################################################################################################################### 93 | # Functions for communicating with the Halite game environment (formerly contained in separate module networking.py # 94 | ##################################################################################################################### 95 | 96 | 97 | def send_string(s): 98 | sys.stdout.write(s) 99 | sys.stdout.write('\n') 100 | sys.stdout.flush() 101 | 102 | 103 | def get_string(): 104 | return sys.stdin.readline().rstrip('\n') 105 | 106 | 107 | def get_init(): 108 | playerID = int(get_string()) 109 | m = GameMap(get_string(), get_string()) 110 | return playerID, m 111 | 112 | 113 | def send_init(name): 114 | send_string(name) 115 | 116 | 117 | def translate_cardinal(direction): 118 | "Translate direction constants used by this Python-based bot framework to that used by the official Halite game environment." 119 | # Cardinal indexing used by this bot framework is 120 | #~ NORTH = 0, EAST = 1, SOUTH = 2, WEST = 3, STILL = 4 121 | # Cardinal indexing used by official Halite game environment is 122 | #~ STILL = 0, NORTH = 1, EAST = 2, SOUTH = 3, WEST = 4 123 | #~ >>> list(map(lambda x: (x+1) % 5, range(5))) 124 | #~ [1, 2, 3, 4, 0] 125 | return (direction + 1) % 5 126 | 127 | 128 | def send_frame(moves): 129 | send_string(' '.join(str(move.square.x) + ' ' + str(move.square.y) + ' ' + str(translate_cardinal(move.direction)) for move in moves)) 130 | -------------------------------------------------------------------------------- /runGame.bat: -------------------------------------------------------------------------------- 1 | .\halite.exe -d "30 30" "python MyBot.py" "python RandomBot.py" 2 | -------------------------------------------------------------------------------- /runGame.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if hash python3 2>/dev/null; then 4 | ./halite -d "30 30" "python3 MyBot.py" "python3 RandomBot.py" 5 | else 6 | ./halite -d "30 30" "python MyBot.py" "python RandomBot.py" 7 | fi 8 | --------------------------------------------------------------------------------