diff options
author | Carlo Zancanaro <carlo@zancanaro.id.au> | 2022-09-25 00:10:13 +1000 |
---|---|---|
committer | Carlo Zancanaro <carlo@zancanaro.id.au> | 2022-09-25 00:38:04 +1000 |
commit | 7daa5284f9eddf6d4b4e7838919e80ce25324bb0 (patch) | |
tree | ac92d9aa97621b4c314441a4cb7cdcab60b4f85e | |
parent | e031af6e5e8324fe4cda66d9597904040b17ca80 (diff) |
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | bot_server.py | 33 | ||||
-rw-r--r-- | capturer.py | 52 | ||||
-rw-r--r-- | example-bots/adaptive.py | 63 | ||||
-rw-r--r-- | example-bots/manhattan.py (renamed from manhattan.py) | 23 | ||||
-rw-r--r-- | example-bots/pathing.py | 40 | ||||
-rw-r--r-- | example-bots/random.py | 11 | ||||
-rw-r--r-- | example-bots/random_valid.py | 30 | ||||
-rw-r--r-- | example-bots/up_bot.py | 11 | ||||
-rw-r--r-- | manifest.scm | 11 | ||||
-rw-r--r-- | maps/arena.txt (renamed from arena.txt) | 0 | ||||
-rw-r--r-- | maps/empty.txt (renamed from empty.txt) | 0 | ||||
-rw-r--r-- | maps/large.txt (renamed from large.txt) | 0 | ||||
-rw-r--r-- | maps/maze.txt (renamed from maze.txt) | 0 | ||||
-rw-r--r-- | maps/small.txt | 11 | ||||
-rw-r--r-- | maps/two.txt (renamed from two.txt) | 0 | ||||
-rw-r--r-- | napoleon.py | 146 | ||||
-rw-r--r-- | network/server.py | 1 | ||||
-rw-r--r-- | peter.py | 15 | ||||
-rw-r--r-- | real-setup.py | 20 | ||||
-rw-r--r-- | requirements.txt | 8 | ||||
-rw-r--r-- | robots/algorithms.pyx | 162 | ||||
-rw-r--r-- | robots/client.py | 37 | ||||
-rw-r--r-- | robots/cursesviewer.py | 20 | ||||
-rw-r--r-- | robots/game.py | 4 | ||||
-rw-r--r-- | robots/server.py | 2 | ||||
-rw-r--r-- | robots/state.py | 41 | ||||
-rw-r--r-- | run_competition.py | 2 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | setup.py | 40 | ||||
-rw-r--r-- | simple.py | 19 | ||||
-rw-r--r-- | up_bot.py | 5 |
32 files changed, 285 insertions, 525 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__
\ No newline at end of file diff --git a/bot_server.py b/bot_server.py new file mode 100644 index 0000000..6d8c72c --- /dev/null +++ b/bot_server.py @@ -0,0 +1,33 @@ +import importlib +import os +import robots +import sys + +if __name__ == '__main__': + if len(sys.argv) < 2: + print(f'Usage: python3 {sys.argv[0]} {{your-name}}') + print(f' your-name: your name, used to identify your server on the network') + print() + print(f'Run this command to host a server advertising all of the bots in the bots/ directory.') + sys.exit(1); + + name, = sys.argv[1:] + server = robots.Server() + server.SERVER_NAME = name + + num_bots = 0 + for filename in os.listdir('bots'): + if filename.endswith('.py'): + bot_name = filename.removesuffix('.py') + module = importlib.import_module(f'bots.{bot_name}') + try: + server.add_simple_bot(module.calculate_orders, bot_name) + num_bots += 1 + except AttributeError: + print(f'Warning: bots/{filename} was missing `calculate_orders` function.', file=sys.stderr) + + if num_bots == 0: + print(f'No bots found: not starting a bot server.', file=sys.stderr) + sys.exit(2) + else: + server.run() diff --git a/capturer.py b/capturer.py deleted file mode 100644 index 54b0be6..0000000 --- a/capturer.py +++ /dev/null @@ -1,52 +0,0 @@ -import random - -import robots - -from robots.algorithms import distance_relaxer -from robots.constants import City -from robots.utils import add_spawns - -class CaptureSpawns(object): - def __init__(self, variance=0.1): - self.iterations = 10 - self.variance = variance - - def __call__(self, whoami, state): - my_robots = state.robots_by_player[whoami] - - # Create a distance matrix. - distances = [] - for y, row in enumerate(state.cities): - output = [] - for x, city in enumerate(row): - d = float('inf') - if city == City.FACTORY: - if state.allegiances.get((x, y)) != whoami: - d = 0 - elif city == City.GHOST: - d = None - output.append(d) - distances.append(output) - - # Find the shortest path to a target from each cell. - predecessors = distance_relaxer(distances) - - # Direct the robots to follow those paths. - results = [] - for x, y, energy in my_robots: - if random.random() < self.variance: - result = random.choice('ULDR') - elif predecessors[y][x]: - result = random.choice(predecessors[y][x]) - elif state.allegiances.get((x, y)) != whoami: - result = 'P' - else: - result = random.choice('UDLR') - results.append(result) - return results - -if __name__ == '__main__': - server = robots.Server() - server.SERVER_NAME = 'Capturer Server' - server.add_bot(CaptureSpawns, 'Capture') - server.run() diff --git a/example-bots/adaptive.py b/example-bots/adaptive.py new file mode 100644 index 0000000..18f8717 --- /dev/null +++ b/example-bots/adaptive.py @@ -0,0 +1,63 @@ +import random + +def move_toward_closest_factory(whoami, width, height, board, rx, ry, capture_types): + if board[rx, ry].allegiance != whoami and board[rx, ry].city in capture_types: + return 'P' + to_do = [ + (rx + 1, ry, 'R'), + (rx - 1, ry, 'L'), + (rx, ry - 1, 'U'), + (rx, ry + 1, 'D') + ] + random.shuffle(to_do) + seen = [] + while to_do: + (x, y, move) = to_do.pop(0) + x %= width + y %= height + if (x, y) in seen or board[x, y].city not in ['+', '.']: + continue + seen.append((x, y)) + if board[x, y].allegiance != whoami and board[x, y].city in capture_types: + return move + else: + to_add = [(x + 1, y, move), + (x - 1, y, move), + (x, y + 1, move), + (x, y - 1, move)] + random.shuffle(to_add) + to_do += to_add + +def calculate_orders(whoami, state): + # In order to start painting, we need to be leading by at least + # this percentage (per opposing player). + leading_factor_per_opponent = 0.1 # 10% + opponents = (len(state.robots_by_player) - 1) + factor = 1 - (leading_factor_per_opponent * opponents) + + orders = [] + + board = state.board + width = state.width + height = state.height + my_robots = state.robots_by_player[whoami] + max_robots = max( + len(robots) + for player, robots in state.robots_by_player.items() + if player != whoami + ) + my_factories = state.factories_by_player[whoami] + max_factories = max( + len(factories) + for player, factories in state.factories_by_player.items() + if player != whoami + ) + if factor * len(my_robots) < max_robots or factor * len(my_factories) < max_factories: + capture_types = ['+'] + else: + capture_types = ['+', '.'] + for rx, ry, energy in my_robots: + move = move_toward_closest_factory(whoami, width, height, board, rx, ry, capture_types) + orders.append(move or random.choice('UDLRP')) + + return orders diff --git a/manhattan.py b/example-bots/manhattan.py index f64b1e2..a65d0e9 100644 --- a/manhattan.py +++ b/example-bots/manhattan.py @@ -1,25 +1,9 @@ import random def distance(x1, y1, x2, y2): - ''' - >>> distance(0, 0, 3, 0) - 3 - >>> distance(1, 1, 3, 3) - 4 - ''' return abs(x1 - x2) + abs(y1 - y2) def direction_to(rx, ry, fx, fy): - ''' - >>> direction_to(0, 0, 3, 0) - 'R' - >>> direction_to(0, 0, 0, 3) - 'D' - >>> direction_to(6, 0, 3, 0) - 'L' - >>> direction_to(0, 6, 0, 3) - 'U' - ''' moves = [] if fx < rx: moves.append('L') @@ -36,7 +20,7 @@ def closest_factory(whoami, state, rx, ry): closest_distance = 9999999 for fx, fy in state.factories: - if state.allegiances.get((fx, fy)) != whoami: + if state[fx, fy].allegiance != whoami: d = distance(rx, ry, fx, fy) if d < closest_distance: closest_factory = (fx, fy) @@ -44,10 +28,11 @@ def closest_factory(whoami, state, rx, ry): return closest_distance, closest_factory -def manhattan(whoami, state): +def calculate_orders(whoami, state): orders = [] - for rx, ry, energy in state.robots_by_player[whoami]: + my_robots = state.robots_by_player[whoami] + for rx, ry, energy in my_robots: distance, factory = closest_factory(whoami, state, rx, ry) if factory is None or random.random() < 0.1: diff --git a/example-bots/pathing.py b/example-bots/pathing.py new file mode 100644 index 0000000..d537940 --- /dev/null +++ b/example-bots/pathing.py @@ -0,0 +1,40 @@ +import random + +def move_toward_closest_factory(whoami, state, rx, ry): + if state[rx, ry].allegiance != whoami and state[rx, ry].city == '+': + return 'P' + to_do = [ + (rx + 1, ry, 'R'), + (rx - 1, ry, 'L'), + (rx, ry - 1, 'U'), + (rx, ry + 1, 'D') + ] + random.shuffle(to_do) + seen = [] + while to_do: + (x, y, move) = to_do.pop(0) + if (x, y) in seen or state[x, y].city == 'X': + continue + seen.append((x, y)) + if state[x, y].allegiance != whoami and state[x, y].city == '+': + return move + else: + to_add = [(x + 1, y, move), + (x - 1, y, move), + (x, y + 1, move), + (x, y - 1, move)] + random.shuffle(to_add) + to_do += to_add + +def calculate_orders(whoami, state): + orders = [] + + my_robots = state.robots_by_player[whoami] + for rx, ry, energy in my_robots: + move = move_toward_closest_factory(whoami, state, rx, ry) + if move and random.random() > 0.1: + orders.append(move) + else: + orders.append(random.choice('UDLRP')) + + return orders diff --git a/example-bots/random.py b/example-bots/random.py new file mode 100644 index 0000000..e61ff15 --- /dev/null +++ b/example-bots/random.py @@ -0,0 +1,11 @@ +import random + +def calculate_orders(whoami, state): + orders = [] + + my_robots = state.robots_by_player[whoami] + for robot in my_robots: + # Make a random move for each robot + orders.append(random.choice('ULDRP')) + + return orders diff --git a/example-bots/random_valid.py b/example-bots/random_valid.py new file mode 100644 index 0000000..0a84b96 --- /dev/null +++ b/example-bots/random_valid.py @@ -0,0 +1,30 @@ +import random + +def calculate_orders(whoami, state): + orders = [] + + my_robots = state.robots_by_player[whoami] + for rx, ry, energy in my_robots: + if state[rx, ry].city == '+' and state[rx, ry].allegiance != whoami: + orders.append('P') + continue + potential_orders = [] + if state[rx, ry].allegiance != whoami: + potential_orders.append('P') + if state[rx + 1, ry].city != 'X': + potential_orders.append('R') + if state[rx - 1, ry].city != 'X': + potential_orders.append('L') + if state[rx, ry + 1].city != 'X': + potential_orders.append('D') + if state[rx, ry - 1].city != 'X': + potential_orders.append('U') + + if potential_orders: + # Make a random move for each robot + orders.append(random.choice(potential_orders)) + else: + # There are no valid moves, so do nothing for this robot + orders.append('-') + + return orders diff --git a/example-bots/up_bot.py b/example-bots/up_bot.py new file mode 100644 index 0000000..282d25b --- /dev/null +++ b/example-bots/up_bot.py @@ -0,0 +1,11 @@ +def calculate_orders(whoami, state): + orders = [] + + # Get the robots you control. + my_robots = state.robots_by_player[whoami] + + # Tell them all to go up. + for robot in my_robots: + orders.append('U') + + return orders diff --git a/manifest.scm b/manifest.scm new file mode 100644 index 0000000..4539292 --- /dev/null +++ b/manifest.scm @@ -0,0 +1,11 @@ +(use-modules (gnu packages)) + +(specifications->manifest + (list "python" + "python-six" + "python-urwid" + "python-pyzmq" + "python-blessings" + "python-logbook" + "python-pygobject" + "python-dbus")) diff --git a/arena.txt b/maps/arena.txt index 03ea50b..03ea50b 100644 --- a/arena.txt +++ b/maps/arena.txt diff --git a/empty.txt b/maps/empty.txt index 105bc1b..105bc1b 100644 --- a/empty.txt +++ b/maps/empty.txt diff --git a/large.txt b/maps/large.txt index 1fbaae8..1fbaae8 100644 --- a/large.txt +++ b/maps/large.txt diff --git a/maps/small.txt b/maps/small.txt new file mode 100644 index 0000000..81c5753 --- /dev/null +++ b/maps/small.txt @@ -0,0 +1,11 @@ +XXXXXXXXXXX +X++.....++X +X+a.XXX.c+X +X.........X +X.X.X+X.X.X +X.X.+++.X.X +X.X.X+X.X.X +X.........X +X+d.XXX.b+X +X++.....++X +XXXXXXXXXXX diff --git a/napoleon.py b/napoleon.py deleted file mode 100644 index 15685eb..0000000 --- a/napoleon.py +++ /dev/null @@ -1,146 +0,0 @@ -# Strategiser -# Goals - -from collections import defaultdict -from functools import lru_cache -from random import choice - -import robots - -from robots.algorithms import distance_relaxer -from robots.constants import City - -def get_close_spawns(whoami, state, threshold): - distances = [] - for y, row in enumerate(state.cities): - distances.append([]) - for x, cell in enumerate(row): - if cell not in City.traversable: - d = None - elif ( - cell == City.FACTORY and - state.allegiances.get((x, y)) != whoami - ): - d = 0 - else: - d = float('inf') - distances[-1].append(d) - - predecessors = distance_relaxer(distances, threshold) - return predecessors, distances - -@lru_cache(maxsize=16) -def closest_unpainted(whoami, state): - distances = [] - for y, row in enumerate(state.cities): - distances.append([]) - for x, cell in enumerate(row): - if cell == City.GHOST: - d = None - elif state.allegiances.get((x, y)) != whoami: - d = 0 - else: - d = float('inf') - distances[-1].append(d) - - return distance_relaxer(distances) - -def good_moves(state, x, y, moves): - return [ - move - for move in moves - if not state.robots[state.expected_position(x, y, move)] - ] - -class Napoleon: - def __init__(self, capturers_frac=1, dampening=0.01): - # (x, y) -> (goal, goal_state) - self.bot_goals = {} - self.capturers_frac = capturers_frac - self.dampening = dampening - - def goal_capture(self, whoami, state, x, y, goal_state): - moves = goal_state - good = good_moves(state, x, y, moves) - if good: - return choice(good) - return choice(moves or 'P') - - def goal_paint(self, whoami, state, x, y, goal_state): - predecessors = closest_unpainted(whoami, state) - moves = predecessors[y][x] - good = good_moves(state, x, y, moves) - if good: - return choice(good) - return choice(moves or 'P') - - def strategise(self, whoami, state): - bot_goals = {} - - robots = state.robots_by_player[whoami] - - if len(state.factories_by_player[whoami]) <= 3: - self.capturers_frac = 1.0 - if len(robots) <= 10: - self.capturers_frac = 1.0 - -# threshold = 10 if early_stages else None - threshold = None - spawns, spawns_dist = get_close_spawns(whoami, state, threshold) - - max_capturers = max(3, int(len(robots) * self.capturers_frac)) - self.capturers_frac = max(0.01, self.capturers_frac - self.dampening) - capturers = { - (x, y) - for x, y, _ in sorted( - robots, - key=lambda r: spawns_dist[r[1]][r[0]], - )[:max_capturers] - if threshold is None or spawns_dist[y][x] <= threshold - } - assert len(capturers) <= max_capturers - - for x, y, energy in robots: - if (x, y) in capturers: - goal = 'capture' - goal_state = spawns[y][x] - else: - goal = 'paint' - goal_state = None - - bot_goals[x, y] = (goal, goal_state) - -# print(whoami, ''.join(v[0].upper() for v, _ in bot_goals.values())) - return bot_goals - - def __call__(self, whoami, state): - self.bot_goals = self.strategise(whoami, state) - - results = {} - - todo = self.bot_goals.keys() - - for _ in range(2): - by_dest = defaultdict(list) - - for x, y in todo: - goal, goal_state = self.bot_goals[x, y] - goal_fn = getattr(self, 'goal_' + goal) - action = goal_fn(whoami, state, x, y, goal_state) - by_dest[state.expected_position(x, y, action)].append((x, y)) - results[x, y] = action - - todo = [] - for robots in by_dest.values(): - if len(robots) > 1: - todo.extend(robots) - - return ''.join( - results[x, y] - for x, y, _ in state.robots_by_player[whoami] - ) - -if __name__ == '__main__': - server = robots.Server() - server.add_bot(Napoleon, 'Napoleon') - server.run() diff --git a/network/server.py b/network/server.py index 9c63ea5..7aea705 100644 --- a/network/server.py +++ b/network/server.py @@ -134,7 +134,6 @@ class Server(object): host = '' domain = '' - help(self.avahi_group) self.avahi_group.AddService( avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), diff --git a/peter.py b/peter.py deleted file mode 100644 index a79e545..0000000 --- a/peter.py +++ /dev/null @@ -1,15 +0,0 @@ -import robots - -from capturer import CaptureSpawns -from simple import random_walk -from napoleon import Napoleon -from manhattan import manhattan -from up_bot import up_bot - -server = robots.Server() -server.add_simple_bot(up_bot, 'Simba') -server.add_simple_bot(random_walk, 'Zazu') -server.add_simple_bot(manhattan, 'Nala') -server.add_bot(CaptureSpawns, 'Scar') -server.add_bot(Napoleon, 'Mufasa') -server.run() diff --git a/real-setup.py b/real-setup.py deleted file mode 100644 index 5a15cf2..0000000 --- a/real-setup.py +++ /dev/null @@ -1,20 +0,0 @@ -from setuptools import find_packages, setup -from Cython.Build import cythonize - -setup( - name='robots', - packages=find_packages(), - ext_modules=cythonize('robots/*.pyx'), - install_requires=[ - 'blessings', - 'cython', - 'logbook', - 'pyzmq', - 'urwid' - ], - entry_points={ - 'console_scripts': [ - 'robots-client = robots.client:main', - ] - }, -) diff --git a/requirements.txt b/requirements.txt index d9b8239..f9d1f00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ --e ../simple-network --e . +blessings==1.7 +Logbook==1.5.3 +PyGObject==3.40.1 +pyzmq==22.3.0 +six==1.16.0 +urwid==2.1.2 diff --git a/robots/algorithms.pyx b/robots/algorithms.pyx deleted file mode 100644 index 3bf6916..0000000 --- a/robots/algorithms.pyx +++ /dev/null @@ -1,162 +0,0 @@ -# cython: language_level=3 - -cimport cython - -import numpy as np -cimport numpy as np - -ITYPE = np.int32 -ctypedef np.int32_t ITYPE_t - -ctypedef cython.bint bool - -INFINITY = float('inf') -FAKE_INFINITY = 1<<31 - 1 - -PREDECESSORS = {} -for i in range(16): - p = [] - if i & 1: - p.append('U') - if i & 2: - p.append('D') - if i & 4: - p.append('L') - if i & 8: - p.append('R') - PREDECESSORS[i] = p - -def distance_relaxer(distances, iterations=None): - width = len(distances[0]) - height = len(distances) - - predecessors = [ - [ - [] - for _ in range(width) - ] - for _ in range(height) - ] - - convert = { - INFINITY: FAKE_INFINITY, - None: -1, - } - unconvert = { - FAKE_INFINITY: INFINITY, - -1: None, - } - _distances = np.array( - [ - [ - convert.get(v, v) - for v in row - ] - for row in distances - ], - dtype=ITYPE, - order='c', - ) - - if iterations is None: - iterations = -1 - - _distance_relaxer( - _distances, - predecessors, - iterations, - width, height, - ) - - for y in range(height): - for x in range(width): - v = _distances[y, x] - distances[y][x] = unconvert.get(v, v) - - return predecessors - -@cython.boundscheck(False) -cdef void _distance_relaxer( - np.ndarray[ITYPE_t, ndim=2, mode='c'] distances, - predecessors, - int iterations, - Py_ssize_t width, - Py_ssize_t height, -): - cdef Py_ssize_t x, y - cdef Py_ssize_t nx, ny - cdef ITYPE_t distance, this_distance - - cdef int i = 0 - cdef bool updated - cdef int p - - while i != iterations: - updated = False - - for y from 0 <= y < height: - for x from 0 <= x < width: - distance = distances[y, x] - if distance == -1: - continue - - p = 0 - - nx = x - ny = (y - 1) % height - this_distance = distances[ny, nx] - if this_distance != -1: - if this_distance != FAKE_INFINITY: - this_distance += 1 - if this_distance < distances[y, x]: - p = 0 - distances[y, x] = this_distance - updated = True - if this_distance == distances[y, x]: - p |= 1 - - nx = x - ny = (y + 1) % height - this_distance = distances[ny, nx] - if this_distance != -1: - if this_distance != FAKE_INFINITY: - this_distance += 1 - if this_distance < distances[y, x]: - p = 0 - distances[y, x] = this_distance - updated = True - if this_distance == distances[y, x]: - p |= 2 - - nx = (x - 1) % width - ny = y - this_distance = distances[ny, nx] - if this_distance != -1: - if this_distance != FAKE_INFINITY: - this_distance += 1 - if this_distance < distances[y, x]: - p = 0 - distances[y, x] = this_distance - updated = True - if this_distance == distances[y, x]: - p |= 4 - - nx = (x + 1) % width - ny = y - this_distance = distances[ny, nx] - if this_distance != -1: - if this_distance != FAKE_INFINITY: - this_distance += 1 - if this_distance < distances[y, x]: - p = 0 - distances[y, x] = this_distance - updated = True - if this_distance == distances[y, x]: - p |= 8 - - if p: - predecessors[y][x] = PREDECESSORS[p] - - if not updated: - break - i += 1 diff --git a/robots/client.py b/robots/client.py index 45b593e..021a11d 100644 --- a/robots/client.py +++ b/robots/client.py @@ -3,7 +3,6 @@ import itertools import os import random import signal -#from string import ascii_lowercase as lowercase import time import zmq @@ -159,7 +158,7 @@ def make_ui(avail_walker, added_walker): lines = content.split('\n') stdout.set_text('\n'.join(lines[-5:])) - frame = urwid.Frame(main, footer=urwid.LineBox(stdout)) + frame = urwid.Frame(main) #, footer=urwid.LineBox(stdout)) return frame, update_stdout def choose_bots(): @@ -256,23 +255,31 @@ def main(): map_, victory_by_combat=len(bots) != 1, ) - for key, server_info, name in bots: -# suffix = ''.join(random.sample(lowercase, 3)) -# fullname = '%s.%s.%s' % ( -# server_info['name'], -# name, -# suffix, -# ) - game.add_bot( - RemoteBot(*key), - name, - ) + + bots_with_names = [ + [RemoteBot(*key), f"{server_info['name']}.{name}"] + for key, server_info, name in bots + ] + + for i, (bot, name) in enumerate(bots_with_names): + same_name_before = 0 + same_name_total = 0 + for j, (_, other_name) in enumerate(bots_with_names): + if name == other_name: + if j < i: + same_name_before += 1 + same_name_total += 1 + if same_name_total > 1: + game.add_bot(bot, f"{name} ({same_name_before + 1})") + else: + game.add_bot(bot, name) viewer = CursesViewer(game) viewer.run() - time.sleep(1) - random.shuffle(bots) + if args.loop: + time.sleep(3) + random.shuffle(bots) if __name__ == '__main__': main() diff --git a/robots/cursesviewer.py b/robots/cursesviewer.py index 96042a2..6fbbaca 100644 --- a/robots/cursesviewer.py +++ b/robots/cursesviewer.py @@ -36,27 +36,29 @@ class CursesViewer: players.append(fn(name)) stdout.append(' '.join(players) + '\n') - width = len(state.board[0]) * 3 + width = state.width * 3 stdout.append(Box.TL + Box.H * width + Box.TR + '\n') - for y, row in enumerate(state.board): + board = state.board + for y in range(state.height): output = [] - for x, cell in enumerate(row): + for x in range(state.width): + cell = board[(x, y)] value = ' ' - if cell['city'] == City.GHOST: + if cell.city == City.GHOST: background = 15 else: - background = self.player_colours.get(cell['allegiance']) + background = self.player_colours.get(cell.allegiance) - if cell['robots']: - (robot_owner, energy), = cell['robots'] + if cell.robots: + (robot_owner, energy), = cell.robots foreground = self.player_colours[robot_owner] if foreground == background: foreground = 15 - if cell['city'] == City.FACTORY: + if cell.city == City.FACTORY: value = '[%s]' % energy else: value = '<%s>' % energy @@ -64,7 +66,7 @@ class CursesViewer: func = self.terminal.color(foreground) value = func(value) - elif cell['city'] == City.FACTORY: + elif cell.city == City.FACTORY: value = '[ ]' if background is not None: diff --git a/robots/game.py b/robots/game.py index 0581020..afc84b6 100644 --- a/robots/game.py +++ b/robots/game.py @@ -222,11 +222,13 @@ class Game: while not self.finished: yield self.state self.next() - if self.time > 400: + if self.time > 10000: break yield self.state def next(self): + # 0. Clear the board cache so we can see the baord after the last moves. + self.state.board_cache = None # 1. You give commands you your robots. actions = self.call_bots() # 2. They perform the requested commands. diff --git a/robots/server.py b/robots/server.py index 65c1136..41b6f33 100644 --- a/robots/server.py +++ b/robots/server.py @@ -8,6 +8,7 @@ import logbook import network from robots.state import GameState +from traceback import print_exc log = logbook.Logger(__name__) @@ -114,6 +115,7 @@ class Server(object): try: result = instance(whoami, state) except Exception: + print_exc() message = 'Exception running instance %s.' % instance_id log.exception(message) return server_error(message) diff --git a/robots/state.py b/robots/state.py index 37d2554..59ee026 100644 --- a/robots/state.py +++ b/robots/state.py @@ -3,6 +3,12 @@ from collections import defaultdict, Counter from robots.constants import City, DIRECTIONS from robots.utils import ceil_div#, immutable +class CityState: + def __init__(self, city, allegiance, robots): + self.city = city + self.allegiance = allegiance + self.robots = robots + class GameState: """The state of a game at a point in time. @@ -31,6 +37,8 @@ class GameState: Each robot is represented by (x, y, energy). """ + self.board_cache = None + def __hash__(self): return id(self) # return hash(immutable(self._authorative_state)) @@ -94,35 +102,34 @@ class GameState: @property def board(self): """ - A 2D list with all available information about each city. - - Each city is represented with a dictionary: + A dict with all available information about each city. - >>> { - ... 'city': robots.constants.City, - ... 'allegiance': player or None, - ... 'robots': [robot] or [], - ... } + Keys are (x, y) tuples. + Each city is represented with a CityState object: """ + if self.board_cache: + return self.board_cache + # TODO: remove this once I've figured out caching. self_robots = self.robots - result = [] + result = {} for y, row in enumerate(self.cities): - result_row = [] for x, city in enumerate(row): allegiance = self.allegiances.get((x, y)) robots = self_robots[x, y] - result_row.append({ - 'city': city, - 'allegiance': allegiance, - 'robots': robots, - }) - result.append(result_row) + result[(x, y)] = CityState(city, allegiance, robots) + self.board_cache = result; return result + def __getitem__(self, coordinates): + x, y = coordinates + x %= self.width + y %= self.height + return self.board[x, y] + @property def width(self): """Width of the map.""" @@ -186,7 +193,7 @@ class GameState: @property def n_cities_to_win(self): """How many cities you need to pledge allegiance to you to win.""" - return ceil_div(self.n_allegiable_cities, self.n_alive_players) + return ceil_div(self.n_allegiable_cities, self.n_alive_players) + 1 def expected_position(self, x, y, action): """ diff --git a/run_competition.py b/run_competition.py new file mode 100644 index 0000000..ccbb328 --- /dev/null +++ b/run_competition.py @@ -0,0 +1,2 @@ +import robots.client +robots.client.main() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 01f6a07..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -setup-requires = Cython diff --git a/setup.py b/setup.py deleted file mode 100644 index cd079e6..0000000 --- a/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# Install dependencies from a "[metadata] setup-requires = ..." section in -# setup.cfg, then run real-setup.py. -# From https://bitbucket.org/dholth/setup-requires - -import sys, os, subprocess, codecs, pkg_resources - -sys.path[0:0] = ['setup-requires'] -pkg_resources.working_set.add_entry('setup-requires') - -try: - import configparser -except: - import ConfigParser as configparser - -def get_requirements(): - if not os.path.exists('setup.cfg'): return - config = configparser.ConfigParser() - config.readfp(codecs.open('setup.cfg', encoding='utf-8')) - setup_requires = config.get('metadata', 'setup-requires') - specifiers = [line.strip() for line in setup_requires.splitlines()] - for specifier in specifiers: - try: - pkg_resources.require(specifier) - except pkg_resources.DistributionNotFound: - yield specifier - -try: - to_install = list(get_requirements()) - if to_install: - subprocess.call([sys.executable, "-m", "pip", "install", - "-t", "setup-requires"] + to_install) -except (configparser.NoSectionError, configparser.NoOptionError): - pass - -# Run real-setup.py -exec(compile(open("real-setup.py").read().replace('\\r\\n', '\\n'), - __file__, - 'exec')) - diff --git a/simple.py b/simple.py deleted file mode 100644 index 1ff37f2..0000000 --- a/simple.py +++ /dev/null @@ -1,19 +0,0 @@ -import random -import sys - -import robots - -def random_walk(whoami, state): - my_robots = state.robots_by_player[whoami] - return ''.join( - random.choice('ULDRP') - for _ in range(len(my_robots)) - ) - -if __name__ == '__main__': - name, = sys.argv[1:] - server = robots.Server() - server.SERVER_NAME = name - server.add_simple_bot(random_walk, 'Bot1') - server.add_simple_bot(random_walk, 'Bot2') - server.run() diff --git a/up_bot.py b/up_bot.py deleted file mode 100644 index 4b2b403..0000000 --- a/up_bot.py +++ /dev/null @@ -1,5 +0,0 @@ -def up_bot(whoami, state): - # Get the number of robots you control. - n_robots = len(state.robots_by_player[whoami]) - # Tell them all to go up. - return 'U' * n_robots |