Repository: ASPP/pelita

Issue:
[discussion] Is it possible to have more than one error per (round, turn)?
If not, then I would suggest to change the error list to a dict with key (round, turn) as it would make checking for errors in the current round simpler. (And an error in the current round would always give you a random move.) Right now, we have a bug that a timeout gives two errors.
I would like to make it a rule that there can be at most one error per turn.

Comments:

Patch:
diff --git a/pelita/game.py b/pelita/game.py
index b42b52ae..1ecf0a00 100644
--- a/pelita/game.py
+++ b/pelita/game.py
@@ -13,7 +13,7 @@ from warnings import warn
 from . import layout
 from .exceptions import FatalException, NonFatalException, NoFoodWarning
 from .gamestate_filters import noiser
-from .layout import initial_positions, get_legal_moves
+from .layout import initial_positions, get_legal_positions
 from .libpelita import get_python_process, SimplePublisher
 from .network import bind_socket, setup_controller
 from .player.team import make_team
@@ -316,7 +316,7 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed=
         layout_name=None,
         team_names=[None] * 2,
         fatal_errors=[[], []],
-        errors=[[], []],
+        errors=[{}, {}],
         whowins=None,
         rnd=Random(seed),
         viewers=[],
@@ -519,6 +519,7 @@ def play_turn(game_state):
     game_state.update(update_round_counter(game_state))
 
     turn = game_state['turn']
+    round = game_state['round']
     team = turn % 2
     # request a new move from the current team
     try:
@@ -552,11 +553,9 @@ def play_turn(game_state):
         # are collected and added to team_errors
         exception_event = {
             'type': e.__class__.__name__,
-            'description': str(e),
-            'turn': game_state['turn'],
-            'round': game_state['round'],
+            'description': str(e)
         }
-        game_state['errors'][team].append(exception_event)
+        game_state['errors'][team][(round, turn)] = exception_event
         position = None
         game_print(turn, f"{type(e).__name__}: {e}")
 
@@ -624,25 +623,34 @@ def apply_move(gamestate, bot_position):
 
     # previous errors
     team_errors = gamestate["errors"][team]
-    # check is step is legal
-    legal_moves = get_legal_moves(walls, gamestate["bots"][gamestate["turn"]])
-    if bot_position not in legal_moves:
-        bad_bot_position = bot_position
-        bot_position = legal_moves[gamestate['rnd'].randint(0, len(legal_moves)-1)]
-        error_dict = {
-            "turn": turn,
-            "round": n_round,
-            "reason": 'illegal move',
-            "bot_position": bot_position
-            }
-        game_print(turn, f"Illegal move {bad_bot_position} not in {sorted(legal_moves)}. Choosing a random move instead: {bot_position}")
-        team_errors.append(error_dict)
+
+    # the allowed moves for the current bot
+    legal_positions = get_legal_positions(walls, gamestate["bots"][gamestate["turn"]])
+
+    # unless we have already made an error, check if we made a legal move
+    if not (n_round, turn) in team_errors:
+        if bot_position not in legal_positions:
+            error_dict = {
+                "reason": 'illegal move',
+                "bot_position": bot_position
+                }
+            # add the error to the team’s errors
+            game_print(turn, f"Illegal position. {bot_position} not in legal positions: {sorted(legal_positions)}.")
+            team_errors[(n_round, turn)] = error_dict
 
     # only execute move if errors not exceeded
     gamestate.update(check_gameover(gamestate))
     if gamestate['gameover']:
         return gamestate
 
+    # Now check if we must make a random move
+    if (n_round, turn) in team_errors:
+        # There was an error for this round and turn
+        # but the game is not over.
+        # We execute a random move
+        bot_position = gamestate['rnd'].choice(legal_positions)
+        game_print(turn, f"Setting a legal position at random: {bot_position}")
+
     # take step
     bots[turn] = bot_position
     _logger.info(f"Bot {turn} moves to {bot_position}.")
diff --git a/pelita/layout.py b/pelita/layout.py
index 14c96011..1ad10138 100644
--- a/pelita/layout.py
+++ b/pelita/layout.py
@@ -430,8 +430,9 @@ def initial_positions(walls):
     return [left[0], right[0], left[1], right[1]]
 
 
-def get_legal_moves(walls, bot_position):
-    """ Returns legal moves given a position.
+def get_legal_positions(walls, bot_position):
+    """ Returns all legal positions that a bot at `bot_position`
+    can go to.
 
      Parameters
     ----------
@@ -443,12 +444,12 @@ def get_legal_moves(walls, bot_position):
     Returns
     -------
     list
-        legal moves.
+        legal positions
 
     Raises
     ------
     ValueError
-        if position invalid or on wall
+        if bot_position invalid or on wall
     """
     width, height = wall_dimensions(walls)
     if not (0, 0) <= bot_position < (width, height):


Test patch:
diff --git a/test/test_game.py b/test/test_game.py
index bc19a73f..1476a615 100644
--- a/test/test_game.py
+++ b/test/test_game.py
@@ -12,7 +12,7 @@ from textwrap import dedent
 import numpy as np
 
 from pelita import game, layout
-from pelita.game import initial_positions, get_legal_moves, apply_move, run_game, setup_game, play_turn
+from pelita.game import initial_positions, get_legal_positions, apply_move, run_game, setup_game, play_turn
 from pelita.player import stepping_player
 
 
@@ -154,23 +154,23 @@ def test_initial_positions_same_in_layout(layout_name):
     out = initial_positions(walls)
     assert out == exp
 
-def test_get_legal_moves_basic():
+def test_get_legal_positions_basic():
     """Check that the output of legal moves contains all legal moves for one example layout"""
     l = layout.get_layout_by_name(layout_name="layout_small_without_dead_ends_100")
     parsed_l = layout.parse_layout(l)
-    legal_moves = get_legal_moves(parsed_l["walls"], parsed_l["bots"][0])
+    legal_positions = get_legal_positions(parsed_l["walls"], parsed_l["bots"][0])
     exp = [(2, 5), (1, 6), (1, 5)]
-    assert legal_moves == exp
+    assert legal_positions == exp
 
 @pytest.mark.parametrize('layout_t', [layout.get_random_layout() for _ in range(50)])
 @pytest.mark.parametrize('bot_idx', (0, 1, 2, 3))
-def test_get_legal_moves_random(layout_t, bot_idx):
+def test_get_legal_positions_random(layout_t, bot_idx):
     """Check that the output of legal moves returns only moves that are 1 field away and not inside a wall"""
     layout_name, layout_string = layout_t # get_random_layout returns a tuple of name and string
     parsed_l = layout.parse_layout(layout_string)
     bot = parsed_l["bots"][bot_idx]
-    legal_moves = get_legal_moves(parsed_l["walls"], bot)
-    for move in legal_moves:
+    legal_positions = get_legal_positions(parsed_l["walls"], bot)
+    for move in legal_positions:
         assert move not in parsed_l["walls"]
         assert  abs((move[0] - bot[0])+(move[1] - bot[1])) <= 1
 
@@ -180,21 +180,23 @@ def test_play_turn_apply_error(turn):
     """check that quits when there are too many errors"""
     game_state = setup_random_basic_gamestate()
     error_dict = {
-        "turn": 0,
-        "round": 0,
         "reason": 'illegal move',
         "bot_position": (1, 2)
     }
     game_state["turn"] = turn
     team = turn % 2
-    game_state["errors"] = [[error_dict, error_dict, error_dict, error_dict],
-                            [error_dict, error_dict, error_dict, error_dict]]
-    illegal_move = game_state["walls"][0]
-    game_state_new = apply_move(game_state, illegal_move)
+    game_state["errors"] = [{(r, t): error_dict for r in (1, 2) for t in (0, 1)},
+                            {(r, t): error_dict for r in (1, 2) for t in (0, 1)}]
+    # we pretend that two rounds have already been played
+    # so that the error dictionaries are sane
+    game_state["round"] = 3
+
+    illegal_position = game_state["walls"][0]
+    game_state_new = apply_move(game_state, illegal_position)
     assert game_state_new["gameover"]
     assert len(game_state_new["errors"][team]) == 5
     assert game_state_new["whowins"] == int(not team)
-    assert set(game_state_new["errors"][team][4].keys()) == set(["turn", "round", "reason", "bot_position"])
+    assert set(game_state_new["errors"][team][(3, turn)].keys()) == set(["reason", "bot_position"])
 
 @pytest.mark.parametrize('turn', (0, 1, 2, 3))
 def test_play_turn_fatal(turn):
@@ -205,22 +207,22 @@ def test_play_turn_fatal(turn):
     fatal_list = [{}, {}]
     fatal_list[team] = {"error":True}
     game_state["fatal_errors"] = fatal_list
-    move = get_legal_moves(game_state["walls"], game_state["bots"][turn])
+    move = get_legal_positions(game_state["walls"], game_state["bots"][turn])
     game_state_new = apply_move(game_state, move[0])
     assert game_state_new["gameover"]
     assert game_state_new["whowins"] == int(not team)
 
 @pytest.mark.parametrize('turn', (0, 1, 2, 3))
-def test_play_turn_illegal_move(turn):
+def test_play_turn_illegal_position(turn):
     """check that illegal moves are added to error dict and bot still takes move"""
     game_state = setup_random_basic_gamestate()
     game_state["turn"] = turn
     team = turn % 2
-    illegal_move = game_state["walls"][0]
-    game_state_new = apply_move(game_state, illegal_move)
+    illegal_position = game_state["walls"][0]
+    game_state_new = apply_move(game_state, illegal_position)
     assert len(game_state_new["errors"][team]) == 1
-    assert set(game_state_new["errors"][team][0].keys()) == set(["turn", "round", "reason", "bot_position"])
-    assert game_state_new["bots"][turn] in get_legal_moves(game_state["walls"], game_state["bots"][turn])
+    assert game_state_new["errors"][team][(1, turn)].keys() == set(["reason", "bot_position"])
+    assert game_state_new["bots"][turn] in get_legal_positions(game_state["walls"], game_state["bots"][turn])
 
 @pytest.mark.parametrize('turn', (0, 1, 2, 3))
 @pytest.mark.parametrize('which_food', (0, 1))
@@ -857,9 +859,9 @@ def test_play_turn_move():
         "fatal_errors": [{}, {}],
         "rnd": random.Random()
         }
-    legal_moves = get_legal_moves(game_state["walls"], game_state["bots"][turn])
-    game_state_new = apply_move(game_state, legal_moves[0])
-    assert game_state_new["bots"][turn] == legal_moves[0]
+    legal_positions = get_legal_positions(game_state["walls"], game_state["bots"][turn])
+    game_state_new = apply_move(game_state, legal_positions[0])
+    assert game_state_new["bots"][turn] == legal_positions[0]
 
 
 
diff --git a/test/test_game_master.py b/test/test_game_master.py
index d8340847..619ec625 100644
--- a/test/test_game_master.py
+++ b/test/test_game_master.py
@@ -98,7 +98,7 @@ class TestGameMaster:
             assert state['bots'] == bot_pos
             state = run_game([stopping_player] * 2, layout_dict=parsed, max_rounds=5)
             assert state['fatal_errors'] == [[], []]
-            assert state['errors'] == [[], []]
+            assert state['errors'] == [{}, {}]
         else:
             with pytest.raises(ValueError):
                 setup_game([stopping_player] * 2, layout_dict=parsed, max_rounds=300)
diff --git a/test/test_layout.py b/test/test_layout.py
index f2be16ee..dfca69dd 100644
--- a/test/test_layout.py
+++ b/test/test_layout.py
@@ -257,13 +257,13 @@ def test_equal_positions():
     assert layout['bots'] == [(1, 1)]*4
 
 
-@pytest.mark.parametrize('pos, legal_moves', [
+@pytest.mark.parametrize('pos, legal_positions', [
     ((2, 2), {(2, 1), (2, 3), (1, 2), (3, 2), (2, 2)}),
     ((1, 1), {(1, 2), (2, 1), (1, 1)}),
     ((4, 2), {(4, 2), (4, 1), (4, 3), (3, 2)}),
     ((4, 1), {(4, 2), (4, 1)})
 ])
-def test_legal_moves(pos, legal_moves):
+def test_legal_positions(pos, legal_positions):
     test_layout = (
         """ ######
             #  # #
@@ -271,7 +271,7 @@ def test_legal_moves(pos, legal_moves):
             #    #
             ###### """)
     parsed = parse_layout(test_layout)
-    assert set(get_legal_moves(parsed['walls'], pos)) == legal_moves
+    assert set(get_legal_positions(parsed['walls'], pos)) == legal_positions
 
 
 @pytest.mark.parametrize('pos', [
@@ -281,7 +281,7 @@ def test_legal_moves(pos, legal_moves):
     (7, 7),
     (3, 1)
 ])
-def test_legal_moves_fail(pos):
+def test_legal_positions_fail(pos):
     test_layout = (
         """ ######
             #  # #
@@ -290,4 +290,4 @@ def test_legal_moves_fail(pos):
             ###### """)
     parsed = parse_layout(test_layout)
     with pytest.raises(ValueError):
-        get_legal_moves(parsed['walls'], pos)
+        get_legal_positions(parsed['walls'], pos)
diff --git a/test/test_players.py b/test/test_players.py
index 2f5bc47b..047ce4d7 100644
--- a/test/test_players.py
+++ b/test/test_players.py
@@ -61,5 +61,5 @@ def test_players(player):
     # ensure that all test players ran correctly
     assert state['fatal_errors'] == [[], []]
     # our test players should never return invalid moves
-    assert state['errors'] == [[], []]
+    assert state['errors'] == [{}, {}]
 
diff --git a/test/test_remote_game.py b/test/test_remote_game.py
index 22d95c85..2951864e 100644
--- a/test/test_remote_game.py
+++ b/test/test_remote_game.py
@@ -30,7 +30,7 @@ def test_remote_call_pelita(remote_teams):
     res, stdout, stderr = libpelita.call_pelita(remote_teams, rounds=30, filter='small', viewer='null', dump=None, seed=None)
     assert res['whowins'] == 1
     assert res['fatal_errors'] == [[], []]
-    assert res['errors'] == [[], []]
+    assert res['errors'] == [{}, {}]
 
 
 def test_remote_run_game(remote_teams):
@@ -44,4 +44,4 @@ def test_remote_run_game(remote_teams):
     state = pelita.game.run_game(remote_teams, max_rounds=30, layout_dict=pelita.layout.parse_layout(layout))
     assert state['whowins'] == 1
     assert state['fatal_errors'] == [[], []]
-    assert state['errors'] == [[], []]
+    assert state['errors'] == [{}, {}]