Parameterize initial state distributions for classic control envs.
diff --git a/gym/envs/classic_control/acrobot.py b/gym/envs/classic_control/acrobot.py
index 39c935706a7..0d05db7aa5b 100644
--- a/gym/envs/classic_control/acrobot.py
+++ b/gym/envs/classic_control/acrobot.py
@@ -23,6 +23,12 @@
 from gym.utils.renderer import Renderer
 
 
+DEFAULT_LOW = -0.1
+DEFAULT_HIGH = 0.1
+LIMIT_LOW = -1.0
+LIMIT_HIGH = 1.0
+
+
 class AcrobotEnv(core.Env):
     """
     ### Description
@@ -187,7 +193,21 @@ def reset(
         options: Optional[dict] = None
     ):
         super().reset(seed=seed)
-        self.state = self.np_random.uniform(low=-0.1, high=0.1, size=(4,)).astype(
+        if options is None:
+            low = DEFAULT_LOW
+            high = DEFAULT_HIGH
+        else:
+            low = options.pop('low', DEFAULT_LOW)
+            high = options.pop('high', DEFAULT_HIGH)
+            # We expect only numerical inputs.
+            assert type(low) == int or float
+            assert type(high) == int or float
+            # Since the same boundaries are used for all observations, we set the
+            # limits according to the most restrictive (cos/sin): (-1., 1.).
+            low = max(low, LIMIT_LOW)
+            high = min(high, LIMIT_HIGH)
+            assert low < high
+        self.state = self.np_random.uniform(low=low, high=high, size=(4,)).astype(
             np.float32
         )
 
diff --git a/gym/envs/classic_control/cartpole.py b/gym/envs/classic_control/cartpole.py
index 0cf969c3319..5b7a14abb1c 100644
--- a/gym/envs/classic_control/cartpole.py
+++ b/gym/envs/classic_control/cartpole.py
@@ -14,6 +14,12 @@
 from gym.utils.renderer import Renderer
 
 
+DEFAULT_LOW = -0.05
+DEFAULT_HIGH = 0.05
+LIMIT_LOW = -0.2
+LIMIT_HIGH = 0.2
+
+
 class CartPoleEnv(gym.Env[np.ndarray, Union[int, np.ndarray]]):
     """
     ### Description
@@ -194,7 +200,23 @@ def reset(
         options: Optional[dict] = None,
     ):
         super().reset(seed=seed)
-        self.state = self.np_random.uniform(low=-0.05, high=0.05, size=(4,))
+        if options is None:
+            low = DEFAULT_LOW
+            high = DEFAULT_HIGH
+        else:
+            low = options.pop('low', DEFAULT_LOW)
+            high = options.pop('high', DEFAULT_HIGH)
+            # We expect only numerical inputs.
+            assert type(low) == int or float
+            assert type(high) == int or float
+            # Since the same boundaries are used for all observations, we set the
+            # limits according to the most restrictive (pole angle). As per the
+            # documentation at the top of the file, we restrict them to be within
+            # (-.2, .2).
+            low = max(low, LIMIT_LOW)
+            high = min(high, LIMIT_HIGH)
+            assert low < high
+        self.state = self.np_random.uniform(low=low, high=high, size=(4,))
         self.steps_beyond_done = None
         self.renderer.reset()
         self.renderer.render_step()
diff --git a/gym/envs/classic_control/continuous_mountain_car.py b/gym/envs/classic_control/continuous_mountain_car.py
index 2f8298cf9de..5506611feb7 100644
--- a/gym/envs/classic_control/continuous_mountain_car.py
+++ b/gym/envs/classic_control/continuous_mountain_car.py
@@ -24,6 +24,12 @@
 from gym.utils.renderer import Renderer
 
 
+DEFAULT_LOW = -0.6
+DEFAULT_HIGH = -0.4
+LIMIT_LOW = -1.2
+LIMIT_HIGH = 0.6
+
+
 class Continuous_MountainCarEnv(gym.Env):
     """
     ### Description
@@ -180,7 +186,20 @@ def reset(
         options: Optional[dict] = None
     ):
         super().reset(seed=seed)
-        self.state = np.array([self.np_random.uniform(low=-0.6, high=-0.4), 0])
+        if options is None:
+            low = DEFAULT_LOW
+            high = DEFAULT_HIGH
+        else:
+            low = options.pop('low', DEFAULT_LOW)
+            high = options.pop('high', DEFAULT_HIGH)
+            # We expect only numerical inputs.
+            assert type(low) == int or float
+            assert type(high) == int or float
+            # MountainCar expects states to be within -1.2 and 0.6.
+            low = max(low, LIMIT_LOW)
+            high = min(high, LIMIT_HIGH)
+            assert low < high
+        self.state = np.array([self.np_random.uniform(low=low, high=high), 0])
         self.renderer.reset()
         self.renderer.render_step()
         if not return_info:
diff --git a/gym/envs/classic_control/mountain_car.py b/gym/envs/classic_control/mountain_car.py
index a2e3f52a0f4..3966079dec3 100644
--- a/gym/envs/classic_control/mountain_car.py
+++ b/gym/envs/classic_control/mountain_car.py
@@ -13,6 +13,12 @@
 from gym.utils.renderer import Renderer
 
 
+DEFAULT_LOW = -0.6
+DEFAULT_HIGH = -0.4
+LIMIT_LOW = -1.2
+LIMIT_HIGH = 0.6
+
+
 class MountainCarEnv(gym.Env):
     """
     ### Description
@@ -154,7 +160,20 @@ def reset(
         options: Optional[dict] = None,
     ):
         super().reset(seed=seed)
-        self.state = np.array([self.np_random.uniform(low=-0.6, high=-0.4), 0])
+        if options is None:
+            low = DEFAULT_LOW
+            high = DEFAULT_HIGH
+        else:
+            low = options.pop('low', DEFAULT_LOW)
+            high = options.pop('high', DEFAULT_HIGH)
+            # We expect only numerical inputs.
+            assert type(low) == int or float
+            assert type(high) == int or float
+            # MountainCar expects states to be within -1.2 and 0.6.
+            low = max(low, LIMIT_LOW)
+            high = min(high, LIMIT_HIGH)
+            assert low < high
+        self.state = np.array([self.np_random.uniform(low=low, high=high), 0])
         self.renderer.reset()
         self.renderer.render_step()
         if not return_info:
diff --git a/gym/envs/classic_control/pendulum.py b/gym/envs/classic_control/pendulum.py
index 33547bb2d49..9ad99523dc2 100644
--- a/gym/envs/classic_control/pendulum.py
+++ b/gym/envs/classic_control/pendulum.py
@@ -11,6 +11,10 @@
 from gym.utils.renderer import Renderer
 
 
+DEFAULT_X = np.pi
+DEFAULT_Y = 1.0
+
+
 class PendulumEnv(gym.Env):
     """
        ### Description
@@ -142,8 +146,24 @@ def reset(
         options: Optional[dict] = None
     ):
         super().reset(seed=seed)
-        high = np.array([np.pi, 1])
-        self.state = self.np_random.uniform(low=-high, high=high)
+        if options is None:
+            high = np.array([np.pi, 1])
+        else:
+            x = options.pop('x', DEFAULT_X)
+            y = options.pop('y', DEFAULT_Y)
+            # We expect only numerical inputs.
+            assert type(x) == int or float
+            assert type(y) == int or float
+            # Since the same boundaries are used for all observations, we set the
+            # limits according to the most restrictive (sin/cos). Since these are
+            # the values that will be used for the `high` variable, we enforce them
+            # to be non-negative: (0., 1.).
+            x = max(0.0, min(1.0, x))
+            y = max(0.0, min(1.0, y))
+            assert x < y
+            high = np.array([x, y])
+        low = -high  # We enforce symmetric limits.
+        self.state = self.np_random.uniform(low=low, high=high)
         self.last_u = None
 
         self.renderer.reset()
diff --git a/tests/envs/test_env_implementation.py b/tests/envs/test_env_implementation.py
index 844d422c12a..46752bec890 100644
--- a/tests/envs/test_env_implementation.py
+++ b/tests/envs/test_env_implementation.py
@@ -1,9 +1,11 @@
 import pytest
+from typing import Optional
 
 import gym
 from gym.envs.box2d import BipedalWalker
 from gym.envs.box2d.lunar_lander import demo_heuristic_lander
 from gym.envs.toy_text.frozen_lake import generate_random_map
+import numpy as np
 
 
 def test_lunar_lander_heuristics():
@@ -80,3 +82,34 @@ def test_frozenlake_dfs_map_generation(map_size: int):
                     if new_frozenlake[new_row][new_col] not in "#H":
                         frontier.append((new_row, new_col))
     raise AssertionError("No path through the frozenlake was found.")
+
+
+@pytest.mark.parametrize("low_high", [None, (-1.0, 1.0), (0., 2.), (-2., 1.)])
+def test_customizable_resets(low_high: Optional[list]):
+    envs = {
+        'acrobot': gym.make('Acrobot-v1'),
+        'cartpole': gym.make('CartPole-v1'),
+        'mountaincar': gym.make('MountainCar-v0'),
+        'mountaincar_continuous': gym.make('MountainCarContinuous-v0'),
+        'pendulum': gym.make('Pendulum-v1'),
+    }
+    for env in envs:
+        # For each environment, make sure we can reset and step without out-of-bound
+        # errors.
+        if low_high is None:
+            envs[env].reset()
+        else:
+            low, high = low_high
+            for i in range(15):
+                if env == 'pendulum':
+                    # Pendulum is initialized a little differently, where we specify the
+                    # x and y values for the upper limit (and lower limit is just the
+                    # negative of it).
+                    envs[env].reset(options={'x': low, 'y': high})
+                else:
+                    envs[env].reset(options={'low': low, 'high': high})
+                    assert np.all((envs[env].state >= low) & (envs[env].state <+ high))
+                if env.endswith('continuous') or env == 'pendulum':
+                    envs[env].step([0])
+                else:
+                    envs[env].step(0)

# Description

This PR adds the ability for users to specify custom limits for the initial state distribution boundaries for classic control environments. Currently, they are hard-coded, but it can be useful for research to be able to set these to different values.

Motivating context: https://twitter.com/pcastr/status/1539368076700962817

Fixes # (issue)

## Type of change

- [ ] New feature (non-breaking change which adds functionality)


# Checklist:

- [x ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `pre-commit run --all-files` (see `CONTRIBUTING.md` instructions to set it up)
- [x ] I have commented my code, particularly in hard-to-understand areas
- [x ] I have made corresponding changes to the documentation
- [x ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [x ] New and existing unit tests pass locally with my changes