summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--robots/constants.py45
-rw-r--r--robots/cursesviewer.py28
-rw-r--r--robots/game.py167
-rw-r--r--robots/state.py105
-rw-r--r--robots/utils.py7
-rw-r--r--simple.py23
6 files changed, 227 insertions, 148 deletions
diff --git a/robots/constants.py b/robots/constants.py
new file mode 100644
index 0000000..4c36cce
--- /dev/null
+++ b/robots/constants.py
@@ -0,0 +1,45 @@
+DIRECTIONS = {
+ 'U': (0, -1),
+ 'D': (0, 1),
+ 'L': (-1, 0),
+ 'R': (1, 0),
+}
+
+class City:
+ NORMAL = '.'
+ GHOST = 'X'
+ FACTORY = '+'
+
+ # Cities which can pledge allegiance.
+ # Allegiable is totally a real word.
+ # No, really, it is.
+ # Why doesn't anyone believe me?!!
+ # /sobs
+ allegiable = (NORMAL, FACTORY)
+
+ # Cities which robots can move into.
+ traversable = (NORMAL, FACTORY)
+
+class Action:
+ UP = 'U'
+ DOWN = 'D'
+ LEFT = 'L'
+ RIGHT = 'R'
+ PROMOTE = 'P'
+ NOTHING = '-'
+
+ all = (UP, DOWN, LEFT, RIGHT, PROMOTE, NOTHING)
+
+class Energy:
+ INITIAL = 6
+ MAXIMUM = 9
+ GAIN = 2
+
+ENERGY_BY_ACTION = {
+ Action.UP: 3,
+ Action.DOWN: 3,
+ Action.LEFT: 3,
+ Action.RIGHT: 3,
+ Action.PROMOTE: 4,
+ Action.NOTHING: 0,
+}
diff --git a/robots/cursesviewer.py b/robots/cursesviewer.py
index 0131894..434df47 100644
--- a/robots/cursesviewer.py
+++ b/robots/cursesviewer.py
@@ -20,31 +20,27 @@ class CursesViewer:
colours = cycle('red blue green yellow magenta cyan'.split())
self.player_colours = dict(zip(
- game.players.keys(),
+ game.state.players,
colours,
))
self.player_colours['*'] = 'white'
- def draw_board(self):
+ def draw_board(self, state):
print(self.terminal.clear, end='')
- robots = {}
- for player, info in self.game.players.items():
- for x, y in info['robots']:
- robots[x, y] = player
-
- width = len(self.game.board[0]) * 2
+ width = len(state.board[0]) * 2
print(Box.TL + Box.H * width + Box.TR)
- for y, row in enumerate(self.game.board):
+ for y, row in enumerate(state.board):
output = []
for x, cell in enumerate(row):
value = ' '
- background = self.player_colours.get(cell)
+ background = self.player_colours.get(cell['allegiance'])
+
+ if cell['robots']:
+ (robot_owner, energy), = cell['robots']
- robot_owner = robots.get((x, y))
- if robot_owner:
foreground = self.player_colours[robot_owner]
if foreground == background:
foreground = 'white'
@@ -64,13 +60,9 @@ class CursesViewer:
def run(self):
limiter = rate_limit(10)
- while not self.game.finished:
+ for state in self.game:
next(limiter)
-
- self.draw_board()
- self.game.next()
-
- self.draw_board()
+ self.draw_board(state)
winners = self.game.finished
if winners is True:
diff --git a/robots/game.py b/robots/game.py
index 82ee5ce..ce343e1 100644
--- a/robots/game.py
+++ b/robots/game.py
@@ -1,30 +1,11 @@
-from collections import defaultdict, OrderedDict
+from collections import defaultdict, deque
from contextlib import contextmanager
from copy import deepcopy
+from robots.constants import Action, City, DIRECTIONS, Energy, ENERGY_BY_ACTION
+from robots.state import GameState
from robots.utils import iter_board
-class Squares:
- EMPTY = '.'
- WALL = '*'
-
-class Actions:
- UP = 'U'
- DOWN = 'D'
- LEFT = 'L'
- RIGHT = 'R'
- PAINT = 'P'
- NOTHING = '-'
-
- all = (UP, DOWN, LEFT, RIGHT, PAINT, NOTHING)
-
-DIRECTIONS = {
- 'U': (0, -1),
- 'D': (0, 1),
- 'L': (-1, 0),
- 'R': (1, 0),
-}
-
@contextmanager
def protected(value, name):
value_copy = deepcopy(value)
@@ -34,7 +15,7 @@ def protected(value, name):
if value_copy != value:
raise RuntimeError('Protected value %s was modified.' % name)
-def _extract_spawn_points(map_):
+def extract_spawn_points(map_):
points = []
for x, y, cell in iter_board(map_):
@@ -44,7 +25,7 @@ def _extract_spawn_points(map_):
pass
else:
points.append((n, (x, y)))
- map_[y][x] = Squares.EMPTY
+ map_[y][x] = City.NORMAL
points.sort()
return [point for n, point in points]
@@ -52,17 +33,12 @@ def _extract_spawn_points(map_):
class Game:
def __init__(self, map_):
self.bots = {}
- self.players = OrderedDict()
-
- self.board = deepcopy(map_)
- self._spawns = _extract_spawn_points(self.board)
+ self.time = 0
- self._painted_tiles = {
- '*': float('inf'), # not used
- '.': self.available_tiles,
- }
+ map_ = deepcopy(map_)
+ self.spawn_points = deque(extract_spawn_points(map_))
- self.time = 0
+ self.state = GameState(map_)
def add_bot(self, bot, name=None):
if name is None:
@@ -74,46 +50,30 @@ class Game:
(name,)
)
- spawn = self._spawns.pop()
-
self.bots[name] = bot
- self.players[name] = {
- 'robots': [spawn],
- 'spawn': spawn,
- 'last_spawn': 0,
- }
- self._painted_tiles[name] = 0
- @property
- def width(self):
- return len(self.board[0])
-
- @property
- def height(self):
- return len(self.board)
-
- @property
- def available_tiles(self):
- n = 0
- for x, y, cell in iter_board(self.board):
- if cell != Squares.WALL:
- n += 1
- return n
+ # Place the initial robot for this player.
+ x, y = self.spawn_points.popleft()
+ self.state.robots_by_player[name] = [
+ (x, y, Energy.INITIAL),
+ ]
@property
def finished(self):
- target = int(round(self.available_tiles * 0.5))
+ target = self.state.n_cities_to_win
winners = []
roboteers = []
- for whoami, info in self.players.items():
- n_tiles = self._painted_tiles[whoami]
- if n_tiles > target:
- winners.append(whoami)
+ n_allegiances_by_player = self.state.n_allegiances_by_player
- if info['robots']:
- roboteers.append(whoami)
+ for name, robots in self.state.robots_by_player.items():
+ n_cities = n_allegiances_by_player[name]
+ if n_cities >= target:
+ winners.append(name)
+
+ if robots:
+ roboteers.append(name)
won = bool(winners)
if len(roboteers) <= 1:
@@ -125,22 +85,21 @@ class Game:
def call_bots(self):
for whoami, bot in self.bots.items():
try:
- with protected(self.players, 'players') as players, \
- protected(self.board, 'board') as board:
- result = bot(whoami, players, board)
+ with protected(self.state, 'state') as state:
+ result = bot(whoami, state)
assert isinstance(result, (str, list, tuple)), \
'Bot did not return a str/list/tuple, got %r' % (result,)
result = str(''.join(result))
- my_robots = self.players[whoami]['robots']
+ my_robots = self.state.robots_by_player[whoami]
assert len(result) == len(my_robots), (
'Bot did not return an action for all robots '
'(%d actions != %d robots)' % (len(result), len(my_robots))
)
for action in result:
- assert action in Actions.all, (
+ assert action in Action.all, (
'Got unexpected action %r' % (action,)
)
@@ -153,47 +112,31 @@ class Game:
yield whoami, bot, action
def apply_actions(self, actions):
- new_robots = defaultdict(list)
- bot_locations = defaultdict(int)
+ robots_by_player = defaultdict(list)
for whoami, bot, action in actions:
- x, y = bot
-
- if action == Actions.PAINT:
- previous = self.board[y][x]
- self._painted_tiles[previous] -= 1
+ x, y, energy = bot
- self.board[y][x] = whoami
- self._painted_tiles[whoami] += 1
+ if action == Action.PROMOTE:
+ self.state.allegiances[x, y] = whoami
- elif action != Actions.NOTHING:
+ elif action != Action.NOTHING:
dx, dy = DIRECTIONS[action]
- nx = (x + dx) % self.width
- ny = (y + dy) % self.height
+ nx = (x + dx) % self.state.width
+ ny = (y + dy) % self.state.height
- if self.board[ny][nx] != Squares.WALL:
+ if self.state.cities[ny][nx] in City.traversable:
x = nx
y = ny
+ else:
+ action = Action.NOTHING
- new_robots[whoami].append((x, y))
- bot_locations[x, y] += 1
+ energy -= ENERGY_BY_ACTION[action]
- for whoami, (x, y) in self.spawn_bots():
- new_robots[whoami].append((x, y))
- bot_locations[x, y] += 1
+ robots_by_player[whoami].append((x, y, energy))
- to_remove = set(
- (x, y)
- for (x, y), count in bot_locations.items()
- if count > 1
- )
-
- for whoami, info in self.players.items():
- info['robots'][:] = [
- (x, y)
- for x, y in new_robots[whoami]
- if (x, y) not in to_remove
- ]
+ for name, robots in self.state.robots_by_player.items():
+ robots[:] = robots_by_player[name]
def get_spawn_time(self, whoami):
n_robots = len(self.players[whoami]['robots'])
@@ -205,27 +148,19 @@ class Game:
)
return int(10 * fraction_unowned) + n_robots
- def spawn_bots(self):
- for whoami, info in self.players.items():
- spawn_time = self.get_spawn_time(whoami)
-# print(whoami, spawn_time)
- time_since_last_spawn = self.time - info['last_spawn']
- n_tiles = self._painted_tiles[whoami]
- if time_since_last_spawn >= spawn_time:
- yield whoami, info['spawn']
- info['last_spawn'] = self.time
+ def __iter__(self):
+ while not self.finished:
+ yield self.state
+ self.next()
+ yield self.state
def next(self):
+ # 1. You give commands you your robots.
actions = self.call_bots()
+ # 2. They perform the requested commands.
self.apply_actions(actions)
-
- import sys
- print(
- *(self.get_spawn_time(whoami) for whoami in self.players),
-# *(len(info['robots']) for info in self.players.values()),
- sep='\t',
- file=sys.stderr
- )
- sys.stderr.flush()
+ # 3. Factories produce new robots.
+ # 4. Combats are resolved.
+ # 5. Each robot gains energy, up to the maximum.
self.time += 1
diff --git a/robots/state.py b/robots/state.py
new file mode 100644
index 0000000..1fe9c26
--- /dev/null
+++ b/robots/state.py
@@ -0,0 +1,105 @@
+from collections import defaultdict, Counter
+
+from robots.constants import City
+from robots.utils import ceil_div
+
+class GameState:
+ """The state of a game at a point in time.
+
+ Instances can be serialized for inter-process communication.
+ """
+
+ def __init__(self, board):
+ # _[y][x] = City
+ self.cities = board
+ # _[x, y] = str (player id)
+ self.allegiances = {}
+ # _[player_id] = [(x, y, energy), ...]
+ self.robots_by_player = {}
+
+ def __hash__(self):
+ return hash(self._authorative_state)
+
+ def __eq__(self, other):
+ if isinstance(other, GameState):
+ return self._authorative_state == other._authorative_state
+
+ @property
+ def _authorative_state(self):
+ return (self.cities, self.allegiances, self.robots_by_player)
+
+ @property
+ def players(self):
+ return self.robots_by_player.keys()
+
+ @property
+ def robots(self):
+ result = defaultdict(list)
+ for player, robots in self.robots_by_player.items():
+ for x, y, energy in robots:
+ result[x, y].append((player, energy))
+ return result
+
+ @property
+ def allegiances_by_player(self):
+ result = defaultdict(list)
+ for (x, y), player in self.allegiances.items():
+ result[player].append((x, y))
+ return result
+
+ @property
+ def board(self):
+ # TODO: remove this once I've figured out caching.
+ self_robots = self.robots
+
+ 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)
+ return result
+
+ @property
+ def width(self):
+ return len(self.cities[0])
+
+ @property
+ def height(self):
+ return len(self.cities)
+
+ @property
+ def n_alive_players(self):
+ """How many players are still alive."""
+ return sum(
+ 1
+ for player, robots in self.robots_by_player.items()
+ if robots
+ )
+
+ @property
+ def n_allegiable_cities(self):
+ """How many cities are capable of pledging allegiance."""
+ return sum(
+ 1
+ for row in self.cities
+ for city in row
+ if city in City.allegiable
+ )
+
+ @property
+ def n_allegiances_by_player(self):
+ """How many cities have pledged allegiance to each player."""
+ return Counter(self.allegiances.values())
+
+ @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)
diff --git a/robots/utils.py b/robots/utils.py
index 544bec7..dff2a3e 100644
--- a/robots/utils.py
+++ b/robots/utils.py
@@ -1,6 +1,13 @@
from random import sample
import time
+def ceil_div(a, b):
+ """Divide a by b, rounding towards infinity."""
+ return -(-a // b)
+
+def ilen(items):
+ return sum(1 for _ in items)
+
def add_spawns(map_, n_spawns):
available = []
for x, y, cell in iter_board(map_):
diff --git a/simple.py b/simple.py
index 932cc09..10dba02 100644
--- a/simple.py
+++ b/simple.py
@@ -2,9 +2,7 @@ from collections import defaultdict
import random
import robots
-
-iter_board = robots.utils.iter_board
-DIRECTIONS = robots.game.DIRECTIONS
+from robots.constants import DIRECTIONS
def shuffled(items):
items = list(items)
@@ -103,8 +101,8 @@ def never_paint(whoami, players, board):
for _ in range(len(my_robots))
)
-def bot(whoami, players, board):
- my_robots = players[whoami]['robots']
+def bot(whoami, state):
+ my_robots = state.robots_by_player[whoami]
return ''.join(
random.choice('ULDRP-')
for _ in range(len(my_robots))
@@ -114,18 +112,15 @@ if __name__ == '__main__':
# random.seed(42)
map_ = robots.border_map(30, 10, 0)
for y in range(8):
- map_[y][10] = '*'
+ map_[y][10] = 'X'
for y in range(11, 2, -1):
- map_[y][20] = '*'
+ map_[y][20] = 'X'
map_[5][5] = '1'
- map_[5][15] = '2'
- map_[5][25] = '3'
+ map_[5][15] = '3'
+ map_[5][25] = '2'
game = robots.Game(map_)
-# game.add_bot(attacker, 'Alice')
-# game.add_bot(attacker, 'Adam')
- game.add_bot(attacker, 'Barry')
- game.add_bot(attacker, 'Bob')
- game.add_bot(never_paint, 'Baldrick')
+ game.add_bot(bot, 'Alice')
+ game.add_bot(bot, 'Bob')
viewer = robots.CursesViewer(game)
viewer.run()