Repository: ASPP/pelita

Issue:
print(bot) should show which enemies are noisy
This will hopefully avoid confusion.

One remark: since we got rid of set_initial in the new-style API, the teams never see their enemies sitting unnoised on their initial positions, which has been a nice (and easy) starting point for filtering. Question: Do we want to be explicit about how the initial positions are fixed (ie. add an example) or do we want them to figure it out themselves?

Comments:

Patch:
diff --git a/layouts/create_pelita_layouts.py b/layouts/create_pelita_layouts.py
deleted file mode 100755
index 1dfb91da..00000000
--- a/layouts/create_pelita_layouts.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python3
-# Use this script to update/regenerate the layouts strings in pelita.layouts.py
-
-import os
-import zlib
-import base64
-import pelita
-
-EXTENSION = '.layout'
-OUTFILENAME = '__layouts.py'
-
-local_dir = os.path.dirname(os.path.realpath(__file__))
-pelita_path = os.path.dirname(pelita.__file__)
-outfile = os.path.join(pelita_path, OUTFILENAME)
-
-layout_entry = '{name} = """{code}"""\n'
-
-content = '### This file is auto-generated. DO NOT EDIT! ###\n'
-# loop through all layout files
-for f in sorted(os.listdir(local_dir)):
-    flname, ext = os.path.splitext(f)
-    if ext != EXTENSION:
-        continue
-    with open(os.path.join(local_dir,f), 'rb') as bytemaze:
-        layout = bytemaze.read()
-
-        layout_name = "layout_" + flname
-        # encode layout string
-        layout_code = base64.encodebytes(zlib.compress(layout)).decode()
-
-        content += layout_entry.format(name=layout_name, code=layout_code)
-
-# write out file in pelita directory
-with open(outfile, 'w') as out:
-    out.write(content)
diff --git a/pelita/layout.py b/pelita/layout.py
index 369da014..df604a43 100644
--- a/pelita/layout.py
+++ b/pelita/layout.py
@@ -117,8 +117,9 @@ def parse_layout(layout_str, allow_enemy_chars=False):
     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.
+    at most 2 enemy characters with the letters "E" and "?". The returned dict will
+    then additionally contain an entry "enemy" which contains these coordinates and
+    an entry "is_noisy" that specifies which of the given enemies is noisy.
     If only one enemy character is given, both will be assumed sitting on the
     same spot. """
 
@@ -161,6 +162,7 @@ def parse_layout(layout_str, allow_enemy_chars=False):
     bots = [None] * num_bots
     if allow_enemy_chars:
         enemy = []
+        noisy_enemy = set()
 
     # iterate through all layouts
     for layout in layout_list:
@@ -178,7 +180,10 @@ def parse_layout(layout_str, allow_enemy_chars=False):
 
         # add the enemy, removing duplicates
         if allow_enemy_chars:
-            enemy = list(set(enemy + items['enemy']))
+            # enemy contains _all_ enemies
+            enemy = list(set(enemy + items['enemy'] + items['noisy_enemy']))
+            # noisy_enemy contains only the noisy enemies
+            noisy_enemy.update(items['noisy_enemy'])
 
         # add the bots
         for bot_idx, bot_pos in enumerate(items['bots']):
@@ -213,6 +218,7 @@ def parse_layout(layout_str, allow_enemy_chars=False):
         # sort the enemy characters
         # be careful, since it may contain None
         out['enemy'] = sorted(enemy, key=lambda x: () if x is None else x)
+        out['is_noisy'] = [e in noisy_enemy for e in out['enemy']]
 
     return out
 
@@ -271,6 +277,7 @@ def parse_single_layout(layout_str, num_bots=4, allow_enemy_chars=False):
     bots = [None] * num_bots
     # enemy positions (only used for team-style layouts)
     enemy = []
+    noisy_enemy = []
 
     # iterate through the grid of characters
     for y, row in enumerate(rows):
@@ -292,6 +299,12 @@ def parse_single_layout(layout_str, num_bots=4, allow_enemy_chars=False):
                     enemy.append(coord)
                 else:
                     raise ValueError(f"Enemy character not allowed.")
+            elif char == '?':
+                # noisy_enemy
+                if allow_enemy_chars:
+                    noisy_enemy.append(coord)
+                else:
+                    raise ValueError(f"Enemy character not allowed.")
             else:
                 # bot
                 try:
@@ -312,11 +325,11 @@ def parse_single_layout(layout_str, num_bots=4, allow_enemy_chars=False):
     food.sort()
     out = {'walls':walls, 'food':food, 'bots':bots}
     if allow_enemy_chars:
-        enemy.sort()
-        out['enemy'] = enemy
+        out['enemy'] = sorted(enemy)
+        out['noisy_enemy'] = sorted(noisy_enemy)
     return out
 
-def layout_as_str(*, walls, food=None, bots=None, enemy=None):
+def layout_as_str(*, walls, food=None, bots=None, enemy=None, is_noisy=None):
     """Given walls, food and bots return a string layout representation
 
     Returns a combined layout string.
@@ -339,6 +352,15 @@ def layout_as_str(*, walls, food=None, bots=None, enemy=None):
     if enemy is None:
         enemy = []
 
+    # if noisy is given, it must be of the same length as enemy
+    if is_noisy is None:
+        noisy_enemies = set()
+    elif len(is_noisy) != len(enemy):
+        raise ValueError("Parameter `noisy` must have same length as `enemy`.")
+    else:
+        # if an enemy is flagged as noisy, we put it into the set of noisy_enemies
+        noisy_enemies = {e for e, e_is_noisy in zip(enemy, is_noisy) if e_is_noisy}
+
     # flag to check if we have overlapping objects
 
     # when need_combined is True, we force the printing of a combined layout
@@ -374,7 +396,10 @@ def layout_as_str(*, walls, food=None, bots=None, enemy=None):
                     if (x, y) in bots:
                         out.write(str(bots.index((x, y))))
                     elif (x, y) in enemy:
-                        out.write("E")
+                        if (x, y) in noisy_enemies:
+                            out.write("?")
+                        else:
+                            out.write("E")
                     else:
                         out.write(' ')
                 else:
@@ -403,7 +428,8 @@ def layout_as_str(*, walls, food=None, bots=None, enemy=None):
             # if an enemy coordinate is None
             # don't put the enemy in the layout
             continue
-        coord_bots[pos] = coord_bots.get(pos, []) + ["E"]
+        enemy_char = '?' if pos in noisy_enemies else 'E'
+        coord_bots[pos] = coord_bots.get(pos, []) + [enemy_char]
 
     # loop through the bot coordinates
     while coord_bots:
diff --git a/pelita/player/team.py b/pelita/player/team.py
index b936994f..5f8f638e 100644
--- a/pelita/player/team.py
+++ b/pelita/player/team.py
@@ -594,7 +594,7 @@ class Bot:
 
         header = ("{blue}{you_blue} vs {red}{you_red}.\n" +
             "Playing on {col} side. Current turn: {turn}. Round: {round}, score: {blue_score}:{red_score}. " +
-            "timeouts: {blue_timeouts}:{red_timeouts}").format(
+            "timeouts: {blue_timeouts}:{red_timeouts}\n").format(
             blue=blue.team_name,
             red=red.team_name,
             turn=bot.turn,
@@ -614,7 +614,8 @@ class Bot:
             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])
+                                   enemy=[e.position for e in bot.enemy],
+                                   is_noisy=[e.is_noisy for e in bot.enemy])
 
             out.write(str(layout))
             return out.getvalue()
@@ -681,7 +682,7 @@ def make_bots(*, walls, team, enemy, round, bot_turn, rng):
     return team_bots[bot_turn]
 
 
-def create_layout(*layout_strings, food=None, bots=None, enemy=None):
+def create_layout(*layout_strings, food=None, bots=None, enemy=None, is_noisy=None):
     """ Create a layout from layout strings with additional food, bots and enemy positions.
 
     Walls must be equal in all layout strings. Food positions will be collected.
@@ -729,4 +730,12 @@ def create_layout(*layout_strings, food=None, bots=None, enemy=None):
                 _check_valid_pos(e, "enemy")
                 parsed_layout['enemy'][idx] = e
 
+    # override is_noisy if given
+    if is_noisy is not None:
+        if not len(is_noisy) == 2:
+            raise ValueError(f"is_noisy must be a list of 2 ({is_noisy})!")
+        for idx, e_is_noisy in enumerate(is_noisy):
+            if e_is_noisy is not None:
+                parsed_layout['is_noisy'][idx] = e_is_noisy
+
     return parsed_layout
diff --git a/pelita/utils.py b/pelita/utils.py
index 813e74c1..b238f1ec 100644
--- a/pelita/utils.py
+++ b/pelita/utils.py
@@ -34,7 +34,7 @@ def load_builtin_layout(layout_name, *, is_blue=True):
 
 
 def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None, seed=None,
-                    food=None, bots=None, enemy=None):
+                    food=None, bots=None, enemy=None, is_noisy=None):
     """Returns the first bot object given a layout.
 
     The returned Bot instance can be passed to a move function to test its return value.
@@ -45,7 +45,7 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
     if score is None:
         score = [0, 0]
 
-    layout = create_layout(layout, food=food, bots=bots, enemy=enemy)
+    layout = create_layout(layout, food=food, bots=bots, enemy=enemy, is_noisy=is_noisy)
     width = max(layout['walls'])[0] + 1
 
     food = split_food(width, layout['food'])
@@ -79,7 +79,7 @@ def setup_test_game(*, layout, game=None, is_blue=True, round=None, score=None,
         'bot_was_killed': [False]*2,
         'error_count': 0,
         'food': food[enemy_index],
-        'is_noisy': [False] * len(layout['enemy']),
+        'is_noisy': layout['is_noisy'],
         'name': "red" if is_blue else "blue"
     }
 


Test patch:
diff --git a/test/test_layout.py b/test/test_layout.py
index 0039b6e2..6b15bf7d 100644
--- a/test/test_layout.py
+++ b/test/test_layout.py
@@ -402,6 +402,14 @@ def test_enemy_raises():
         #  #
         ####
         """, [None, None]), # this will set both to None
+    ("""
+        ####
+        # E#
+        ####
+        ####
+        #??#
+        ####
+        """, [(1, 1), (2, 1)]), # two enemies in two layouts with duplication and question marks
 ])
 def test_enemy_positions(layout, enemy_pos):
     if enemy_pos is None:
diff --git a/test/test_utils.py b/test/test_utils.py
new file mode 100644
index 00000000..0262fb16
--- /dev/null
+++ b/test/test_utils.py
@@ -0,0 +1,50 @@
+
+from pelita import utils
+
+import pytest
+
+@pytest.mark.parametrize('is_blue', [True, False])
+def test_setup_test_game(is_blue):
+    layout = utils.load_builtin_layout('small_without_dead_ends_001', is_blue=is_blue)
+    test_game = utils.setup_test_game(layout=layout, is_blue=is_blue)
+
+    if is_blue:
+        assert test_game.position == (1, 5)
+        assert test_game.other.position == (1, 6)
+        assert test_game.enemy[0].position == (16, 1)
+        assert test_game.enemy[1].position == (16, 2)
+    else:
+        assert test_game.position == (16, 2)
+        assert test_game.other.position == (16, 1)
+        assert test_game.enemy[0].position == (1, 5)
+        assert test_game.enemy[1].position == (1, 6)
+
+    # load_builtin_layout loads unnoised enemies
+    assert test_game.enemy[0].is_noisy is False
+    assert test_game.enemy[1].is_noisy is False
+
+
+@pytest.mark.parametrize('is_blue', [True, False])
+def test_setup_test_game(is_blue):
+    # Test that is_noisy is set properly
+    layout = """
+    ##################
+    #. ... .##.     ?#
+    # # #  .  .### # #
+    # # ##. E .      #
+    #      .   .## # #
+    #0# ###.  .  # # #
+    #1     .##. ... .#
+    ##################
+    """
+    test_game = utils.setup_test_game(layout=layout, is_blue=is_blue)
+
+    assert test_game.position == (1, 5)
+    assert test_game.other.position == (1, 6)
+    assert test_game.enemy[0].position == (8, 3)
+    assert test_game.enemy[1].position == (16, 1)
+
+    # load_builtin_layout loads unnoised enemies
+    assert test_game.enemy[0].is_noisy is False
+    assert test_game.enemy[1].is_noisy is True
+