Repository: AmiiThinks/driving_gridworld

Issue:
Enforce a hard limit on the speed limit in `Road` to the number of rows + 1
If the speed limit is larger than this, then the physical plausibility of the similar breaks, because the number of possible obstacle encounters across a fixed distance can depend on the car's speed and the range of its headlights (the number of rows).

Discussion:

Patch:
diff --git a/driving_gridworld/road.py b/driving_gridworld/road.py
index cb519ef..559362f 100644
--- a/driving_gridworld/road.py
+++ b/driving_gridworld/road.py
@@ -142,13 +142,12 @@ def combinations(iterable, r, collection=tuple):
 
 
 class Road(object):
-    def __init__(self, num_rows, car, obstacles, speed_limit):
-        if speed_limit < car.speed:
+    def __init__(self, num_rows, car, obstacles):
+        if num_rows + 1 < car.speed:
             raise ValueError("Car's speed above speed limit!")
         self._num_rows = num_rows
         self._num_columns = 4
         self._car = car
-        self._speed_limit = speed_limit
         self._obstacles = obstacles
         self._available_spaces = {}
         for pos in product(range(0, self._car.speed), range(4)):
@@ -159,6 +158,20 @@ class Road(object):
                 if disallowed_position in self._available_spaces:
                     del self._available_spaces[disallowed_position]
 
+    def speed_limit(self):
+        '''The hard speed limit on this road.
+
+        Taking the `UP` action when traveling at the speed limit has no effect.
+
+        Set according to the headlight range since overdriving the
+        headlights too much breaks the physical plausibility of the game
+        due to the way we reusing obstacles to simulate arbitrarily long
+        roads with many obstacles. This is not too much of a restriction
+        though because even overdriving the headlights by one unit is
+        completely unsafe.
+        '''
+        return self._num_rows + 1
+
     def obstacle_outside_car_path(self, obstacle):
         return (obstacle.col < 0 or obstacle.col >= self._num_columns
                 or obstacle.row >= self._num_rows)
@@ -198,7 +211,7 @@ class Road(object):
                    state. The reward function is deterministic.
         '''
 
-        next_car = self._car.next(action, self._speed_limit)
+        next_car = self._car.next(action, self.speed_limit())
 
         for positions, reveal_indices in (
                 self.every_combination_of_revealed_obstacles()):
@@ -225,8 +238,7 @@ class Road(object):
             reward += self._car.reward()
             if self._car.col == 0 or self._car.col == 3:
                 reward -= 4 * self._car.speed
-            next_road = self.__class__(self._num_rows, next_car,
-                                       next_obstacles, self._speed_limit)
+            next_road = self.__class__(self._num_rows, next_car, next_obstacles)
             yield (next_road, prob, reward)
 
     def to_key(self, show_walls=False):


Test patch:
diff --git a/test/road_test.py b/test/road_test.py
index ae22a47..d8aeb36 100644
--- a/test/road_test.py
+++ b/test/road_test.py
@@ -9,9 +9,8 @@ import pytest
 def test_transition_probs_without_obstacles_are_always_1():
     num_rows = 4
     obstacles = []
-    speed_limit = 1
     car_inst = Car(0, 0, 1)
-    road_test = Road(num_rows, car_inst, obstacles, speed_limit)
+    road_test = Road(num_rows, car_inst, obstacles)
 
     for a in ACTIONS:
         for next_state, prob, reward in road_test.successors(a):
@@ -21,9 +20,7 @@ def test_transition_probs_without_obstacles_are_always_1():
 @pytest.mark.parametrize("obst", [Bump(0, 0), Pedestrian(0, 0)])
 def test_no_obstacles_revealed_is_the_only_valid_set_of_revealed_obstacles_when_all_obstacles_already_on_road(obst):
     num_rows = 2
-    speed_limit = 1
-
-    road_test = Road(num_rows, Car(1, 1, 1), [obst], speed_limit)
+    road_test = Road(num_rows, Car(1, 1, 1), [obst])
     patient = [
         (positions, reveal_indices)
         for positions, reveal_indices in
@@ -36,9 +33,7 @@ def test_no_obstacles_revealed_is_the_only_valid_set_of_revealed_obstacles_when_
 @pytest.mark.parametrize("action", ACTIONS)
 def test_transition_probs_with_one_obstacle_are_1(obst, action):
     num_rows = 2
-    speed_limit = 1
-
-    road_test = Road(num_rows, Car(1, 1, 1), [obst], speed_limit)
+    road_test = Road(num_rows, Car(1, 1, 1), [obst])
     probs = [
         prob
         for next_state, prob, reward in road_test.successors(action)
@@ -50,9 +45,7 @@ def test_transition_probs_with_one_obstacle_are_1(obst, action):
 @pytest.mark.parametrize("action", ACTIONS)
 def test_transition_probs_with_invisible_obstacle(obst, action):
     num_rows = 2
-    speed_limit = 1
-
-    road_test = Road(num_rows, Car(1, 1, 1), [obst], speed_limit)
+    road_test = Road(num_rows, Car(1, 1, 1), [obst])
     probs = [
         prob
         for next_state, prob, reward in road_test.successors(action)
@@ -72,9 +65,8 @@ def test_transition_probs_with_invisible_obstacle(obst, action):
 def test_driving_faster_gives_a_larger_reward(action, current_speed):
     num_rows = 4
     obstacles = []
-    speed_limit = 4
     car = Car(0, 1, current_speed)
-    road_test = Road(num_rows, car, obstacles, speed_limit)
+    road_test = Road(num_rows, car, obstacles)
     for next_state, prob, reward in road_test.successors(action):
         assert reward == float(current_speed)
 
@@ -82,12 +74,10 @@ def test_driving_faster_gives_a_larger_reward(action, current_speed):
 def test_road_cannot_start_with_car_going_faster_than_speed_limit():
     num_rows = 4
     obstacles = []
-    speed_limit = 1
-    current_speed = 2
+    current_speed = 6
     car = Car(0, 0, current_speed)
-
     with pytest.raises(ValueError):
-        road_test = Road(num_rows, car, obstacles, speed_limit)
+        road_test = Road(num_rows, car, obstacles)
 
 
 @pytest.mark.parametrize("car", [Car(0, 0, 1), Car(0, 3, 1)])
@@ -95,20 +85,28 @@ def test_road_cannot_start_with_car_going_faster_than_speed_limit():
 def test_receive_negative_reward_for_driving_off_the_road(car, action):
     num_rows = 4
     obstacles = []
-    speed_limit = 2
-    road_test = Road(num_rows, car, obstacles, speed_limit)
+    road_test = Road(num_rows, car, obstacles)
     for next_state, prob, reward in road_test.successors(action):
         assert reward < 0
+
+
 @pytest.mark.parametrize("obst", [Bump(-1, -1), Pedestrian(0, -1)])
 @pytest.mark.parametrize("action", ACTIONS)
 @pytest.mark.parametrize("speed", [1, 2, 3])
 def test_number_of_successors_invisible_obstacle_and_variable_speeds(
     obst, action, speed):
     num_rows = 2
-    speed_limit = 3
-    road_test = Road(num_rows, Car(1, 1, speed), [obst], speed_limit)
+    road_test = Road(num_rows, Car(1, 1, speed), [obst])
     probs = [
         prob
         for next_state, prob, reward in road_test.successors(action)
     ]
     assert len(probs) == 4 * speed + 1
+
+
+def test_speed_limit_equals_number_of_rows_plus_one():
+    num_rows = 2
+    obstacles = []
+    car = Car(0, 0, 1)
+    road_test = Road(num_rows, car, obstacles)
+    assert road_test.speed_limit() == num_rows + 1