Repository: ASPP/pelita

Issue:
parse_layout should fail when a bot is defined on different coordinates
    from pelita.layout import parse_layout
    layout = """
        ######
        #000.#
        #.111#
        ###### """
    print(parse_layout(layout)['bots'])
    [(3, 1), (4, 2), None, None]

should raise `ValueError`

Discussion:

Patch:
diff --git a/pelita/layout.py b/pelita/layout.py
index 1ad10138..6fde0227 100644
--- a/pelita/layout.py
+++ b/pelita/layout.py
@@ -9,12 +9,6 @@ except SyntaxError as err:
     print("Invalid syntax in __layouts module. Pelita will not be able to use built-in layouts.")
     print(err)
 
-class Layout:
-    pass
-
-class LayoutEncodingException(Exception):
-    """ Signifies a problem with the encoding of a layout. """
-    pass
 
 def get_random_layout(filter=''):
     """ Return a random layout string from the available ones.
@@ -98,7 +92,7 @@ def get_layout_by_name(layout_name):
         # thus reraise as ValueError with appropriate error message.
         raise ValueError("Layout: '%s' is not known." % ke.args)
 
-def parse_layout(layout_str):
+def parse_layout(layout_str, allow_enemy_chars=False):
     """Parse a layout string
 
     Return a dict
@@ -130,8 +124,19 @@ def parse_layout(layout_str):
      #  .  3#
      ########
 
-     In this case, bot '0' and bot '2' are on top of each other at position (1,1)
-    """
+    In this case, bot '0' and bot '2' are on top of each other at position (1,1)
+
+    If `allow_enemy_chars` is True, we additionally allow for the definition of
+    at most 2 enemy characters with the letter "E". The returned dict will then
+    additionally contain an entry "enemy" which contains these coordinates.
+    If only one enemy character is given, both will be assumed sitting on the
+    same spot. """
+
+    if allow_enemy_chars:
+        num_bots = 2
+    else:
+        num_bots = 4
+
     layout_list = []
     start = False
     for i, line in enumerate(layout_str.splitlines()):
@@ -160,24 +165,68 @@ def parse_layout(layout_str):
         # the last layout has not been closed, complain here!
         raise ValueError(f"Layout does not end with a row of walls (line: {i})!")
 
-    # initialize walls, food and bots from the first layout
-    out = parse_single_layout(layout_list.pop(0))
+    # set empty default values
+    walls = []
+    food = []
+    bots = [None] * num_bots
+    if allow_enemy_chars:
+        enemy = []
+
+    # iterate through all layouts
     for layout in layout_list:
-        items = parse_layout(layout)
+        items = parse_single_layout(layout, num_bots=num_bots, allow_enemy_chars=allow_enemy_chars)
+        # initialize walls from the first layout
+        if not walls:
+            walls = items['walls']
+
         # walls should always be the same
-        if items['walls'] != out['walls']:
+        if items['walls'] != walls:
             raise ValueError('Walls are not equal in all layouts!')
+
         # add the food, removing duplicates
-        out['food'] = list(set(out['food'] + items['food']))
+        food = list(set(food + items['food']))
+
+        # add the enemy, removing duplicates
+        if allow_enemy_chars:
+            enemy = list(set(enemy + items['enemy']))
+
         # add the bots
         for bot_idx, bot_pos in enumerate(items['bots']):
             if bot_pos:
-                # this bot position is not None, overwrite whatever we had before
-                out['bots'][bot_idx] = bot_pos
+                # this bot position is not None, overwrite whatever we had before, unless
+                # it already holds a different coordinate
+                if bots[bot_idx] and bots[bot_idx] != bot_pos:
+                    raise ValueError(f"Cannot set bot {bot_idx} to position {bot_pos} (already at {bots[bot_idx]}).")
+                bots[bot_idx] = bot_pos
+
+    if allow_enemy_chars:
+        # validate that we have at most two enemies
+        if len(enemy) > 2:
+            raise ValueError(f"More than two enemies defined: {enemy}!")
+        elif len(enemy) == 2:
+            # do nothing
+            pass
+        elif len(enemy) == 1:
+            # we use the position for both enemies
+            enemy = [enemy[0], enemy[0]]
+        else:
+            enemy = [None, None]
+
+    # build parsed layout, ensuring walls and food are sorted
+    out = {
+        'walls': sorted(walls),
+        'food': sorted(food),
+        'bots': bots
+    }
+
+    if allow_enemy_chars:
+        # sort the enemy characters
+        # be careful, since it may contain None
+        out['enemy'] = sorted(enemy, key=lambda x: () if x is None else x)
 
     return out
 
-def parse_single_layout(layout_str):
+def parse_single_layout(layout_str, num_bots=4, allow_enemy_chars=False):
     """Parse a single layout from a string
 
     See parse_layout for details about valid layout strings.
@@ -228,8 +277,10 @@ def parse_single_layout(layout_str):
     height = len(rows)
     walls = []
     food = []
-    # bot positions (we assume 4 bots)
-    bots = [None]*4
+    # bot positions
+    bots = [None] * num_bots
+    # enemy positions (only used for team-style layouts)
+    enemy = []
 
     # iterate through the grid of characters
     for y, row in enumerate(rows):
@@ -245,22 +296,37 @@ def parse_single_layout(layout_str):
             elif char == ' ':
                 # empty
                 continue
+            elif char == 'E':
+                # enemy
+                if allow_enemy_chars:
+                    enemy.append(coord)
+                else:
+                    raise ValueError(f"Enemy character not allowed.")
             else:
                 # bot
                 try:
-                    # we expect an 0<=index<=3
+                    # we expect an 0<=index<=num_bots
                     bot_idx = int(char)
                     if bot_idx >= len(bots):
                         # reuse the except below
                         raise ValueError
                 except ValueError:
                     raise ValueError(f"Unknown character {char} in maze!")
+
+                # bot_idx is a valid character.
+                if bots[bot_idx]:
+                    # bot_idx has already been set before
+                    raise ValueError(f"Cannot set bot {bot_idx} to position {coord} (already at {bots[bot_idx]}).")
                 bots[bot_idx] = coord
     walls.sort()
     food.sort()
-    return {'walls':walls, 'food':food, 'bots':bots}
+    out = {'walls':walls, 'food':food, 'bots':bots}
+    if allow_enemy_chars:
+        enemy.sort()
+        out['enemy'] = enemy
+    return out
 
-def layout_as_str(*, walls, food=None, bots=None):
+def layout_as_str(*, walls, food=None, bots=None, enemy=None):
     """Given walls, food and bots return a string layout representation
 
     Returns a combined layout string.
@@ -279,21 +345,27 @@ def layout_as_str(*, walls, food=None, bots=None):
     width = max(walls)[0] + 1
     height = max(walls)[1] + 1
 
+    # enemy is optional
+    if enemy is None:
+        enemy = []
 
     # flag to check if we have overlapping objects
 
     # when need_combined is True, we force the printing of a combined layout
     # string:
     # - the first layout will have walls and food
-    # - subsequent layouts will have walls and bots
+    # - subsequent layouts will have walls and bots (and enemies, if given)
     # You'll get as many layouts as you have overlapping bots
     need_combined = False
 
+    # combine bots an enemy lists
+    bots_and_enemy = bots + enemy if enemy else bots
+
     # first, check if we have overlapping bots
-    if len(set(bots)) != len(bots):
+    if len(set(bots_and_enemy)) != len(bots_and_enemy):
         need_combined = True
     else:
-        need_combined = any(coord in food for coord in bots)
+        need_combined = any(coord in food for coord in bots_and_enemy)
     # then, check that bots are not overlapping with food
 
     out = io.StringIO()
@@ -311,6 +383,8 @@ def layout_as_str(*, walls, food=None, bots=None):
                     # we won't need a combined layout later
                     if (x, y) in bots:
                         out.write(str(bots.index((x, y))))
+                    elif (x, y) in enemy:
+                        out.write("E")
                     else:
                         out.write(' ')
                 else:
@@ -333,6 +407,14 @@ def layout_as_str(*, walls, food=None, bots=None):
         # if still no bot was seen here we have to start with an empty list
         coord_bots[pos] = coord_bots.get(pos, []) + [str(idx)]
 
+    # add enemies to mapping
+    for pos in enemy:
+        if pos is None:
+            # if an enemy coordinate is None
+            # don't put the enemy in the layout
+            continue
+        coord_bots[pos] = coord_bots.get(pos, []) + ["E"]
+
     # loop through the bot coordinates
     while coord_bots:
         for y in range(height):
@@ -358,6 +440,28 @@ def layout_as_str(*, walls, food=None, bots=None):
     return out.getvalue()
 
 
+def layout_for_team(layout, is_blue=True):
+    """ Converts a layout dict with 4 bots to a layout
+    from the view of the specified team.
+    """
+    if "enemy" in layout:
+        raise ValueError("Layout is already in team-style.")
+
+    if is_blue:
+        bots = layout['bots'][0::2]
+        enemy = layout['bots'][1::2]
+    else:
+        bots = layout['bots'][1::2]
+        enemy = layout['bots'][0::2]
+
+    return {
+        'walls': layout['walls'][:],
+        'food': layout['food'][:],
+        'bots': bots,
+        'enemy': enemy,
+    }
+
+
 def wall_dimensions(walls):
     """ Given a list of walls, returns a tuple of (width, height)."""
     width = max(walls)[0] + 1
diff --git a/pelita/player/team.py b/pelita/player/team.py
index 58b3efe6..a6a41f58 100644
--- a/pelita/player/team.py
+++ b/pelita/player/team.py
@@ -1,20 +1,20 @@
 
 import collections
-from functools import reduce
-from io import StringIO
 import logging
 import os
-from pathlib import Path
 import random
 import subprocess
 import traceback
+from functools import reduce
+from io import StringIO
+from pathlib import Path
 
 import zmq
 
-from .. import libpelita, layout
+from .. import layout, libpelita
 from ..exceptions import PlayerDisconnected, PlayerTimeout
-from ..network import ZMQConnection, ZMQClientError, ZMQReplyTimeout, ZMQUnreachablePeer
-
+from ..layout import layout_as_str, parse_layout, wall_dimensions
+from ..network import ZMQClientError, ZMQConnection, ZMQReplyTimeout, ZMQUnreachablePeer
 
 _logger = logging.getLogger(__name__)
 
@@ -599,10 +599,10 @@ class Bot:
         with StringIO() as out:
             out.write(header)
 
-            layout = Layout(walls=bot.walls[:],
-                            food=bot.food + bot.enemy[0].food,
-                            bots=[b.position for b in bot._team],
-                            enemy=[e.position for e in bot.enemy])
+            layout = layout_as_str(walls=bot.walls[:],
+                                   food=bot.food + bot.enemy[0].food,
+                                   bots=[b.position for b in bot._team],
+                                   enemy=[e.position for e in bot.enemy])
 
             out.write(str(layout))
             return out.getvalue()
@@ -669,214 +669,6 @@ def make_bots(*, walls, team, enemy, round, bot_turn, rng):
     return team_bots[bot_turn]
 
 
-# @dataclass
-class Layout:
-    def __init__(self, walls, food, bots, enemy):
-        if not food:
-            food = []
-
-        if not bots:
-            bots = [None, None]
-
-        if not enemy:
-            enemy = [None, None]
-
-        # input validation
-        for pos in [*food, *bots, *enemy]:
-            if pos:
-                if len(pos) != 2:
-                    raise ValueError("Items must be tuples of length 2.")
-                if pos in walls:
-                    raise ValueError("Item at %r placed on walls." % (pos,))
-                else:
-                    walls_width = max(walls)[0] + 1
-                    walls_height = max(walls)[1] + 1
-                    if not (0 <= pos[0] < walls_width) or not (0 <= pos[1] < walls_height):
-                        raise ValueError("Item at %r not in bounds." % (pos,))
-
-
-        if len(bots) > 2:
-            raise ValueError("Too many bots given.")
-
-        self.walls = sorted(walls)
-        self.food = sorted(food)
-        self.bots = bots
-        self.enemy = enemy
-        self.initial_positions = self.guess_initial_positions(self.walls)
-
-    def guess_initial_positions(self, walls):
-        """ Returns the free positions that are closest to the bottom left and
-        top right corner. The algorithm starts searching from (1, -2) and (-2, 1)
-        respectively and uses the manhattan distance for judging what is closest.
-        On equal distances, a smaller distance in the x value is preferred.
-        """
-        walls_width = max(walls)[0] + 1
-        walls_height = max(walls)[1] + 1
-
-        left_start = (1, walls_height - 2)
-        left_initials = []
-        right_start = (walls_width - 2, 1)
-        right_initials = []
-
-        dist = 0
-        while len(left_initials) < 2:
-            # iterate through all possible x distances (inclusive)
-            for x_dist in range(dist + 1):
-                y_dist = dist - x_dist
-                pos = (left_start[0] + x_dist, left_start[1] - y_dist)
-                # if both coordinates are out of bounds, we stop
-                if not (0 <= pos[0] < walls_width) and not (0 <= pos[1] < walls_height):
-                    raise ValueError("Not enough free initial positions.")
-                # if one coordinate is out of bounds, we just continue
-                if not (0 <= pos[0] < walls_width) or not (0 <= pos[1] < walls_height):
-                    continue
-                # check if the new value is free
-                if not pos in walls:
-                    left_initials.append(pos)
-
-                if len(left_initials) == 2:
-                    break
-
-            dist += 1
-
-        dist = 0
-        while len(right_initials) < 2:
-            # iterate through all possible x distances (inclusive)
-            for x_dist in range(dist + 1):
-                y_dist = dist - x_dist
-                pos = (right_start[0] - x_dist, right_start[1] + y_dist)
-                # if both coordinates are out of bounds, we stop
-                if not (0 <= pos[0] < walls_width) and not (0 <= pos[1] < walls_height):
-                    raise ValueError("Not enough free initial positions.")
-                # if one coordinate is out of bounds, we just continue
-                if not (0 <= pos[0] < walls_width) or not (0 <= pos[1] < walls_height):
-                    continue
-                # check if the new value is free
-                if not pos in walls:
-                    right_initials.append(pos)
-
-                if len(right_initials) == 2:
-                    break
-
-            dist += 1
-
-        # lower indices start further away
-        left_initials.reverse()
-        right_initials.reverse()
-        return left_initials, right_initials
-
-    def merge(self, other):
-        """ Merges `self` with the `other` layout.
-        """
-
-        if not self.walls:
-            self.walls = other.walls
-        if self.walls != other.walls:
-            raise ValueError("Walls are not equal.")
-
-        self.food += other.food
-        # remove duplicates
-        self.food = list(set(self.food))
-
-        # update all newer bot positions
-        for idx, b in enumerate(other.bots):
-            if b:
-                self.bots[idx] = b
-
-        # merge all enemies and then take the last 2
-        enemies = [e for e in [*self.enemy, *other.enemy] if e is not None]
-        self.enemy = enemies[-2:]
-        # if self.enemy smaller than 2, we pad with None again
-        for _ in range(2 - len(self.enemy)):
-            self.enemy.append(None)
-
-        # return our merged self
-        return self
-
-    def _repr_html_(self):
-        walls = self.walls
-        walls_width = max(walls)[0] + 1
-        walls_height = max(walls)[1] + 1
-        with StringIO() as out:
-            out.write("<table>")
-            for y in range(walls_height):
-                out.write("<tr>")
-                for x in range(walls_width):
-                    if (x, y) in walls:
-                        bg = 'style="background-color: {}"'.format(
-                            "rgb(94, 158, 217)" if x < walls_width // 2 else
-                            "rgb(235, 90, 90)")
-                    elif (x, y) in self.initial_positions[0]:
-                        bg = 'style="background-color: #ffffcc"'
-                    elif (x, y) in self.initial_positions[1]:
-                        bg = 'style="background-color: #ffffcc"'
-                    else:
-                        bg = ""
-                    out.write("<td %s>" % bg)
-                    if (x, y) in walls: out.write("#")
-                    if (x, y) in self.food: out.write('<span style="color: rgb(247, 150, 213)">●</span>')
-                    for idx, pos in enumerate(self.bots):
-                        if pos == (x, y):
-                            out.write(str(idx))
-                    for pos in self.enemy:
-                        if pos == (x, y):
-                            out.write('E')
-                    out.write("</td>")
-                out.write("</tr>")
-            out.write("</table>")
-            return out.getvalue()
-
-    def __str__(self):
-        walls = self.walls
-        walls_width = max(walls)[0] + 1
-        walls_height = max(walls)[1] + 1
-        with StringIO() as out:
-            out.write('\n')
-            # first, print walls and food
-            for y in range(walls_height):
-                for x in range(walls_width):
-                    if (x, y) in walls: out.write('#')
-                    elif (x, y) in self.food: out.write('.')
-                    else: out.write(' ')
-                out.write('\n')
-            out.write('\n')
-            # print walls and bots
-
-            # Do we have bots/enemies sitting on each other?
-
-            # assign bots to their positions
-            bots = {}
-            for pos in self.enemy:
-                bots[pos] = bots.get(pos, []) + ['E']
-            for idx, pos in enumerate(self.bots):
-                bots[pos] = bots.get(pos, []) + [str(idx)]
-            # strip all None positions from bots
-            try:
-                bots.pop(None)
-            except KeyError:
-                pass
-
-            while bots:
-                for y in range(walls_height):
-                    for x in range(walls_width):
-                        if (x, y) in walls: out.write('#')
-                        elif (x, y) in bots:
-                            elem = bots[(x, y)].pop(0)
-                            out.write(elem)
-                            # cleanup
-                            if len(bots[(x, y)]) == 0:
-                                bots.pop((x, y))
-
-                        else: out.write(' ')
-                    out.write('\n')
-                out.write('\n')
-            return out.getvalue()
-
-    def __eq__(self, other):
-        return ((self.walls, self.food, self.bots, self.enemy, self.initial_positions) ==
-                (other.walls, other.food, other.bots, self.enemy, other.initial_positions))
-
-
 def create_layout(*layout_strings, food=None, bots=None, enemy=None):
     """ Create a layout from layout strings with additional food, bots and enemy positions.
 
@@ -887,106 +679,42 @@ def create_layout(*layout_strings, food=None, bots=None, enemy=None):
     ======
     ValueError
         If walls are not equal in all layouts
+        If enemy argument is not a list of two
     """
 
-    # layout_strings can be a list of strings or one huge string
-    # with many layouts after another
-    layouts = [
-        load_layout(layout)
-        for layout_str in layout_strings
-        for layout in split_layout_str(layout_str)
-    ]
-    merged = reduce(lambda x, y: x.merge(y), layouts)
-    additional_layout = Layout(walls=merged.walls, food=food, bots=bots, enemy=enemy)
-    merged.merge(additional_layout)
-    return merged
-
-def split_layout_str(layout_str):
-    """ Turns a layout string containing many layouts into a list
-    of simple layouts.
-    """
-    out = []
-    current_layout = []
-    for row in layout_str.splitlines():
-        stripped = row.strip()
-        if not stripped:
-            # found an empty line
-            # if we have a current_layout, append it to out
-            # and reset it
-            if current_layout:
-                out.append(current_layout)
-                current_layout = []
-            continue
-        # non-empty line: append to current_layout
-        current_layout.append(row)
-
-    # We still have a current layout at the end: append
-    if current_layout:
-        out.append(current_layout)
-
-    return ['\n'.join(l) for l in out]
-
-def load_layout(layout_str):
-    """ Loads a *single* (partial) layout from a string. """
-    build = []
-    width = None
-    height = None
-
-    food = []
-    bots = [None, None]
-    enemy = []
-
-    for row in layout_str.splitlines():
-        stripped = row.strip()
-        if not stripped:
-            continue
-        if width is not None:
-            if len(stripped) != width:
-                raise ValueError("Layout has differing widths.")
-        width = len(stripped)
-        build.append(stripped)
-
-    height = len(build)
-    data=list("".join(build))
-    def idx_to_coord(idx):
-        """ Maps a 1-D index to a 2-D coord given a width and height. """
-        return (idx % width, idx // width)
-
-    # Check that the layout is surrounded with walls
-    for idx, char in enumerate(data):
-        x, y = idx_to_coord(idx)
-        if x == 0 or x == width - 1:
-            if not char == '#':
-                raise ValueError(f"Layout not surrounded with # at ({x}, {y}).")
-        if y == 0 or y == height - 1:
-            if not char == '#':
-                raise ValueError(f"Layout not surrounded with # at ({x}, {y}).")
-
-    walls = []
-    # extract the non-wall values from mesh
-    for idx, char in enumerate(data):
-        coord = idx_to_coord(idx)
-        # We know that each char is only one character, so it is
-        # either wall or something else
-        if '#' in char:
-            walls.append(coord)
-        # free: skip
-        elif ' ' in char:
-            continue
-        # food
-        elif '.' in char:
-            food.append(coord)
-        # other
-        else:
-            if 'E' in char:
-                # We can have several undefined enemies
-                enemy.append(coord)
-            elif '0' in char:
-                bots[0] = coord
-            elif '1' in char:
-                bots[1] = coord
-            else:
-                raise ValueError("Unknown character %s in maze." % char)
+    layout_str = "\n\n".join(layout_strings)
+    parsed_layout = parse_layout(layout_str, allow_enemy_chars=True)
+
+    width, height = wall_dimensions(parsed_layout['walls'])
 
-    walls = sorted(walls)
-    return Layout(walls, food, bots, enemy)
+    def _check_valid_pos(pos, item):
+        if pos in parsed_layout['walls']:
+            raise ValueError(f"{item} must not be on wall (given: {pos})!")
+        if not ((0 <= pos[0] < width) and (0 <= pos[1] < height)):
+            raise ValueError(f"{item} is outside of maze (given: {pos} but dimensions are {width}x{height})!")
+
+    # if additional food was supplied, we add it
+    if food:
+        for f in food:
+            _check_valid_pos(f, "food")
+        parsed_layout['food'] = sorted(list(set(food + parsed_layout['food'])))
+
+    # override bots if given and not None
+    if bots is not None:
+        if len(bots) > 2:
+            raise ValueError(f"bots must not be more than 2 ({bots})!")
+        for idx, pos in enumerate(bots):
+            if pos is not None:
+                _check_valid_pos(pos, "bot")
+                parsed_layout['bots'][idx] = pos
+
+    # override enemies if given
+    if enemy is not None:
+        if not len(enemy) == 2:
+            raise ValueError(f"enemy must be a list of 2 ({enemy})!")
+        for idx, e in enumerate(enemy):
+            if e is not None:
+                _check_valid_pos(e, "enemy")
+                parsed_layout['enemy'][idx] = e
+
+    return parsed_layout
diff --git a/pelita/utils/__init__.py b/pelita/utils/__init__.py
index 4d63bbe4..c2172229 100644
--- a/pelita/utils/__init__.py
+++ b/pelita/utils/__init__.py
@@ -3,11 +3,10 @@ import random
 from ..player.team import create_layout, make_bots
 from ..graph import Graph
 
-def split_food(layout):
-    width = max(layout.walls)[0] + 1
 
+def split_food(width, food):
     team_food = [set(), set()]
-    for pos in layout.food:
+    for pos in food:
         idx = pos[0] // (width // 2)
         team_food[idx].add(pos)
     return team_food
@@ -26,7 +25,9 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
         score = [0, 0]
 
     layout = create_layout(layout, food=food, bots=bots, enemy=enemy)
-    food = split_food(layout)
+    width = max(layout['walls'])[0] + 1
+
+    food = split_food(width, layout['food'])
 
     if is_blue:
         team_index = 0
@@ -38,7 +39,7 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
     rng = random.Random(seed)
 
     team = {
-        'bot_positions': layout.bots[:],
+        'bot_positions': layout['bots'][:],
         'team_index': team_index,
         'score': score[team_index],
         'kills': [0]*2,
@@ -49,7 +50,7 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
         'name': "blue" if is_blue else "red"
     }
     enemy = {
-        'bot_positions': layout.enemy[:],
+        'bot_positions': layout['enemy'][:],
         'team_index': enemy_index,
         'score': score[enemy_index],
         'kills': [0]*2,
@@ -57,11 +58,11 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
         'bot_eaten': [False]*2,
         'error_count': 0,
         'food': food[enemy_index],
-        'is_noisy': [False] * len(layout.enemy),
+        'is_noisy': [False] * len(layout['enemy']),
         'name': "red" if is_blue else "blue"
     }
 
-    bot = make_bots(walls=layout.walls[:],
+    bot = make_bots(walls=layout['walls'][:],
                     team=team,
                     enemy=enemy,
                     round=round,


Test patch:
diff --git a/test/test_layout.py b/test/test_layout.py
index dfca69dd..e8c7d4b7 100644
--- a/test/test_layout.py
+++ b/test/test_layout.py
@@ -138,6 +138,41 @@ def test_combined_layouts_empty_lines():
     from_single = parse_single_layout(layouts)
     assert from_combined == from_single
 
+def test_duplicate_bots_forbidden():
+    layouts = """
+                 ####
+                 #11#
+                 ####
+                 """
+    with pytest.raises(ValueError):
+        parse_layout(layouts)
+
+def test_duplicate_bots_forbidden_multiple():
+    layouts = """
+                 ####
+                 # 1#
+                 ####
+
+                 ####
+                 #1 #
+                 ####
+                 """
+    with pytest.raises(ValueError):
+        parse_layout(layouts)
+
+def test_duplicate_bots_allowed():
+    layouts = """
+                 ####
+                 # 1#
+                 ####
+
+                 ####
+                 # 1#
+                 ####
+                 """
+    parsed_layout = parse_layout(layouts)
+    assert parsed_layout['bots'][1] == (2, 1)
+
 def test_combined_layouts_broken_lines():
     layouts = """
                  ####
@@ -291,3 +326,113 @@ def test_legal_positions_fail(pos):
     parsed = parse_layout(test_layout)
     with pytest.raises(ValueError):
         get_legal_positions(parsed['walls'], pos)
+
+def test_enemy_raises():
+    layouts = """
+        ####
+        #E1#
+        ####
+
+        ####
+        #1 #
+        ####
+        """
+    with pytest.raises(ValueError):
+        parse_layout(layouts)
+
+@pytest.mark.parametrize('layout,enemy_pos', [
+    ("""
+        ####
+        #E #
+        ####
+        """, [(1, 1), (1, 1)]), # one enemy sets both coordinates
+    ("""
+        ####
+        #EE#
+        ####
+        """, [(1, 1), (2, 1)]), # two enemies
+    ("""
+        ####
+        #E #
+        ####
+        ####
+        #E #
+        ####
+        """, [(1, 1), (1, 1)]), # two enemies two layouts on the same spot
+    ("""
+        ####
+        #E #
+        ####
+        ####
+        # E#
+        ####
+        """, [(1, 1), (2, 1)]), # two enemies in two layouts
+    ("""
+        ####
+        # E#
+        ####
+        ####
+        #E #
+        ####
+        """, [(1, 1), (2, 1)]), # two enemies in two layouts (list is sorted)
+    ("""
+        ####
+        # E#
+        ####
+        ####
+        #EE#
+        ####
+        """, [(1, 1), (2, 1)]), # two enemies in two layouts with duplication
+    ("""
+        #######
+        #E E E#
+        #######
+        """, None), # this will raise ValueError
+    ("""
+        ####
+        #  #
+        ####
+        """, [None, None]), # this will set both to None
+])
+def test_enemy_positions(layout, enemy_pos):
+    if enemy_pos is None:
+        with pytest.raises(ValueError):
+            parse_layout(layout, allow_enemy_chars=True)
+    else:
+        assert parse_layout(layout, allow_enemy_chars=True)['enemy'] == enemy_pos
+
+def test_layout_for_team():
+    # test that we can convert a layout to team-style
+    l1 = """
+    ####
+    #01#
+    #32#
+    #..#
+    ####
+    """
+    blue1 = layout_as_str(**layout_for_team(parse_layout(l1), is_blue=True))
+    red1 = layout_as_str(**layout_for_team(parse_layout(l1), is_blue=False))
+
+    assert blue1 == """\
+####
+#0E#
+#E1#
+#..#
+####
+"""
+
+    assert red1 == """\
+####
+#E0#
+#1E#
+#..#
+####
+"""
+
+
+    # cannot convert layout that is already in team-style
+    with pytest.raises(ValueError):
+        layout_for_team(parse_layout(blue1))
+
+    with pytest.raises(ValueError):
+        layout_for_team(parse_layout(red1))
diff --git a/test/test_team.py b/test/test_team.py
index ada43a44..6b9cd97f 100644
--- a/test/test_team.py
+++ b/test/test_team.py
@@ -1,8 +1,8 @@
 import pytest
 
-from pelita.layout import parse_layout, get_random_layout, initial_positions
+from pelita.layout import parse_layout, get_random_layout, initial_positions, layout_as_str
 from pelita.game import run_game, setup_game, play_turn
-from pelita.player.team import Team, split_layout_str, create_layout
+from pelita.player.team import Team, create_layout
 from pelita.utils import setup_test_game
 
 def stopping(bot, state):
@@ -12,166 +12,154 @@ def randomBot(bot, state):
     legal = bot.legal_positions[:]
     return bot.random.choice(legal), state
 
-class TestLayout:
-    layout="""
-    ########
-    # ###E0#
-    #1E    #
-    ########
-    """
-    layout2="""
-    ########
-    # ###  #
-    # . ...#
-    ########
-    """
-
-    def test_split_layout(self):
-        layout = split_layout_str(self.layout)
-        assert len(layout) == 1
-        assert layout[0].strip() != ""
-
-        mini = """####
-                  #  #
-                  ####"""
-        layout = split_layout_str(mini)
-        assert len(layout) == 1
-        assert layout[0].strip() != ""
-
-    def test_load(self):
-        layout = create_layout(self.layout, self.layout2)
-        assert layout.bots == [(6, 1), (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-
-    def test_concat(self):
-        layout = create_layout(self.layout + self.layout2)
-        assert layout.bots == [(6, 1), (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-
-    def test_load1(self):
-        layout = create_layout(self.layout)
-        assert layout.bots == [(6, 1), (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-
-    def test_equal_positions(self):
-        layout_str = """
-            ########
-            #0###  #
-            # . ...#
-            ########
-
-            ########
-            #1###  #
-            # . ...#
-            ########
-
-            ########
-            #E###  #
-            # . ...#
-            ########
-
-            ########
-            #E###  #
-            # . ...#
-            ########
-        """
-        layout = create_layout(layout_str)
-        assert layout.bots == [(1, 1), (1, 1)]
-        assert layout.enemy ==  [(1, 1), (1, 1)]
-        setup_test_game(layout=layout_str)
-
-    def test_define_after(self):
-        layout = create_layout(self.layout, food=[(1, 1)], bots=[None, None], enemy=None)
-        assert layout.bots == [(6, 1), (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-        layout = create_layout(self.layout, food=[(1, 1)], bots=[None, (1, 2)], enemy=None)
-        assert layout.bots == [(6, 1), (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-
-        layout = create_layout(self.layout, food=[(1, 1)], bots=[None, (1, 2)], enemy=[(5, 1)])
-        assert layout.enemy == [(2, 2), (5, 1)]
-
-        layout = create_layout(self.layout2, food=[(1, 1)], bots=[None, (1, 2)], enemy=[(5, 1), (2, 2)])
-        assert layout.bots == [None, (1, 2)]
-        assert layout.enemy == [(5, 1), (2, 2)]
-
-        with pytest.raises(ValueError):
-            # placed bot on walls
-            layout = create_layout(self.layout2, food=[(1, 1)], bots=[(0, 1), (1, 2)], enemy=[(5, 1), (2, 2)])
-
-        with pytest.raises(ValueError):
-            # placed bot outside maze
-            layout = create_layout(self.layout2, food=[(1, 1)], bots=[(1, 40), (1, 2)], enemy=[(5, 1), (2, 2)])
-
-        with pytest.raises(ValueError):
-            # too many bots
-            layout = create_layout(self.layout2, food=[(1, 1)], bots=[(1, 1), (1, 2), (2, 2)], enemy=[(5, 1), (2, 2)])
-
-    def test_repr(self):
-        layout = create_layout(self.layout, food=[(1, 1)], bots=[None, None], enemy=None)
-        assert layout._repr_html_()
-        str1 = str(create_layout(self.layout, food=[(1, 1)], bots=[None, None], enemy=None))
-        assert str1 == """
+layout1="""
 ########
-#.###  #
-#      #
+# ###E0#
+#1E    #
+########
+"""
+layout2="""
 ########
+# ###  #
+# . ...#
+########
+"""
+
+
+def test_load():
+    layout = create_layout(layout1, layout2)
+    assert layout['bots'] == [(6, 1), (1, 2)]
+    assert layout['enemy'] == [(2, 2), (5, 1)]
+
+def test_concat():
+    layout = create_layout(layout1 + layout2)
+    assert layout['bots'] == [(6, 1), (1, 2)]
+    assert layout['enemy'] == [(2, 2), (5, 1)]
+
+def test_load1():
+    layout = create_layout(layout1)
+    assert layout['bots'] == [(6, 1), (1, 2)]
+    assert layout['enemy'] == [(2, 2), (5, 1)]
+
+def test_equal_positions():
+    layout_str = """
+        ########
+        #0###  #
+        # . ...#
+        ########
+
+        ########
+        #1###  #
+        # . ...#
+        ########
 
+        ########
+        #E###  #
+        # . ...#
+        ########
+
+        ########
+        #E###  #
+        # . ...#
+        ########
+    """
+    layout = create_layout(layout_str)
+    assert layout['bots'] == [(1, 1), (1, 1)]
+    assert layout['enemy'] ==  [(1, 1), (1, 1)]
+    setup_test_game(layout=layout_str)
+
+def test_define_after():
+    layout = create_layout(layout1, food=[(1, 1)], bots=[None, None], enemy=None)
+    assert layout['bots'] == [(6, 1), (1, 2)]
+    assert layout['enemy'] == [(2, 2), (5, 1)]
+    layout = create_layout(layout1, food=[(1, 1)], bots=[None, (1, 1)], enemy=None)
+    assert layout['bots'] == [(6, 1), (1, 1)]
+    assert layout['enemy'] == [(2, 2), (5, 1)]
+
+    with pytest.raises(ValueError):
+        # must define all enemies
+        layout = create_layout(layout1, food=[(1, 1)], bots=[None, (1, 2)], enemy=[(5, 1)])
+
+    layout = create_layout(layout2, food=[(1, 1)], bots=[None, (1, 2)], enemy=[(5, 1), (2, 2)])
+    assert layout['bots'] == [None, (1, 2)]
+    assert layout['enemy'] == [(5, 1), (2, 2)]
+
+    with pytest.raises(ValueError):
+        # placed bot on walls
+        layout = create_layout(layout2, food=[(1, 1)], bots=[(0, 1), (1, 2)], enemy=[(5, 1), (2, 2)])
+
+    with pytest.raises(ValueError):
+        # placed bot outside maze
+        layout = create_layout(layout2, food=[(1, 1)], bots=[(1, 40), (1, 2)], enemy=[(5, 1), (2, 2)])
+
+    with pytest.raises(ValueError):
+        # placed bot outside maze
+        layout = create_layout(layout2, food=[(1, 1)], bots=[(40, 40), (1, 2)], enemy=[(5, 1), (2, 2)])
+
+    with pytest.raises(ValueError):
+        # placed bot outside maze
+        layout = create_layout(layout2, food=[(1, 1)], bots=[(-40, 4), (1, 2)], enemy=[(5, 1), (2, 2)])
+
+    with pytest.raises(ValueError):
+        # too many bots
+        layout = create_layout(layout2, food=[(1, 1)], bots=[(1, 1), (1, 2), (2, 2)], enemy=[(5, 1), (2, 2)])
+
+def test_repr():
+    layout = create_layout(layout1, food=[(1, 1)], bots=[None, None], enemy=None)
+    str1 = layout_as_str(**layout)
+    assert str1 == """\
 ########
-# ###E0#
+#.###E0#
 #1E    #
 ########
-
 """
 
-        layout_merge = create_layout(self.layout, food=[(1, 1)], bots=[(1, 2), (1, 2)], enemy=[(1, 1), (1, 1)])
-        str2 = str(layout_merge)
-        assert str2 == """
+    layout_merge = create_layout(layout1, food=[(1, 1)], bots=[(1, 2), (1, 2)], enemy=[(1, 1), (1, 1)])
+    str2 = layout_as_str(**layout_merge)
+    assert str2 == """\
 ########
 #.###  #
 #      #
 ########
-
 ########
 #E###  #
 #0     #
 ########
-
 ########
 #E###  #
 #1     #
 ########
-
 """
-        # load again
-        assert create_layout(str2) == layout_merge
+    # load again
+    assert create_layout(str2) == layout_merge
 
-    def test_solo_bot(self):
-        l = """
-        ########
-        #.###  #
-        #      #
-        ########
+def test_two_enemies():
+    # single E evaluates to two enemies
+    l = """
+    ########
+    #.###  #
+    #      #
+    ########
 
-        ########
-        # ###  #
-        #0E    #
-        ########
-        """
-        layout = create_layout(l, food=[(1, 1)], bots=[None, None], enemy=None)
-        assert layout._repr_html_()
-        str1 = str(create_layout(l, food=[(1, 1)], bots=[None, None], enemy=None))
-        assert str1 == """
+    ########
+    # ###  #
+    #0E    #
+    ########
+    """
+    str1 = layout_as_str(**create_layout(l, food=[(1, 1)], bots=[None, None], enemy=None))
+    assert str1 == """\
 ########
 #.###  #
 #      #
 ########
-
 ########
 # ###  #
 #0E    #
 ########
-
+########
+# ###  #
+# E    #
+########
 """