Skip to content
78 changes: 54 additions & 24 deletions gym/envs/box2d/car_racing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
BORDER = 8 / SCALE
BORDER_MIN_COUNT = 4

ROAD_COLOR = [0.4, 0.4, 0.4]


class FrictionDetector(contactListener):
def __init__(self, env, lap_complete_percent):
Expand Down Expand Up @@ -63,9 +61,8 @@ def _contact(self, contact, begin):
if not tile:
return

tile.color[0] = ROAD_COLOR[0]
tile.color[1] = ROAD_COLOR[1]
tile.color[2] = ROAD_COLOR[2]
# inherit tile color from env
tile.color = self.env.norm_road_color
if not obj or "tiles" not in obj.__dict__:
return
if begin:
Expand Down Expand Up @@ -128,10 +125,15 @@ class CarRacing(gym.Env, EzPickle):
receive -100 reward and die.

### Arguments
There are no arguments supported in constructing the environment.
`lap_complete_percent` dictates the percentage of tiles that must be visited by
the agent before a lap is considered complete.

Passing `hardcore=True` enabled the domain randomized variant of the environment.
In this scenario, the background and track colours are different on every reset.

### Version History
- v0: Current version
- v1: Current version (0.23.1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess v0 can be labelled as the original version; v1 will come live with 0.24.0 at the earliest, so probably better to put that (seeing as 0.23.1 is already out)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, fixed it

- v0: (reference needed)

### References
- Chris Campbell (2014), http://www.iforce2d.net/b2dtut/top-down-car.
Expand All @@ -145,8 +147,16 @@ class CarRacing(gym.Env, EzPickle):
"render_fps": FPS,
}

def __init__(self, verbose=1, lap_complete_percent=0.95):
def __init__(
self,
verbose: bool = True,
lap_complete_percent: float = 0.95,
hardcore: bool = False,
):
EzPickle.__init__(self)
self.hardcore = hardcore
self._init_colors()

self.contactListener_keepref = FrictionDetector(self, lap_complete_percent)
self.world = Box2D.b2World((0, 0), contactListener=self.contactListener_keepref)
self.screen = None
Expand Down Expand Up @@ -183,6 +193,22 @@ def _destroy(self):
self.road = []
self.car.destroy()

def _init_colors(self):
if self.hardcore:
# domain randomize the bg and grass colour
self.norm_road_color = self.np_random.uniform(0, 0.8, size=3)

self.bg_color = self.np_random.uniform(0, 210, size=3)

self.grass_color = np.copy(self.bg_color)
idx = self.np_random.integers(3)
self.grass_color[idx] += 20
else:
# default colours
self.norm_road_color = np.array([0.4, 0.4, 0.4])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to store the colors with two different representations? (one is 0-1, the other is 0-255)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, fixed it

self.bg_color = np.array([102, 204, 102])
self.grass_color = np.array([102, 230, 102])

def _create_track(self):
CHECKPOINTS = 12

Expand Down Expand Up @@ -280,7 +306,7 @@ def _create_track(self):
elif pass_through_start and i1 == -1:
i1 = i
break
if self.verbose == 1:
if self.verbose:
print("Track generation: %i..%i -> %i-tiles track" % (i1, i2, i2 - i1))
assert i1 != -1
assert i2 != -1
Expand Down Expand Up @@ -339,7 +365,7 @@ def _create_track(self):
t = self.world.CreateStaticBody(fixtures=self.fd_tile)
t.userData = t
c = 0.01 * (i % 3)
t.color = [ROAD_COLOR[0] + c, ROAD_COLOR[1] + c, ROAD_COLOR[2] + c]
t.color = self.norm_road_color + c
t.road_visited = False
t.road_friction = 1.0
t.idx = i
Expand Down Expand Up @@ -385,12 +411,13 @@ def reset(
self.t = 0.0
self.new_lap = False
self.road_poly = []
self._init_colors()

while True:
success = self._create_track()
if success:
break
if self.verbose == 1:
if self.verbose:
print(
"retry to generate track (normal if there are not many"
"instances of this message)"
Expand All @@ -402,7 +429,7 @@ def reset(
else:
return self.step(None)[0], {}

def step(self, action):
def step(self, action: np.ndarray):
if action is not None:
self.car.steer(-action[0])
self.car.gas(action[1])
Expand Down Expand Up @@ -432,7 +459,7 @@ def step(self, action):

return self.state, step_reward, done, {}

def render(self, mode="human"):
def render(self, mode: str = "human"):
import pygame

pygame.font.init()
Expand All @@ -459,13 +486,13 @@ def render(self, mode="human"):
trans = pygame.math.Vector2((scroll_x, scroll_y)).rotate_rad(angle)
trans = (WINDOW_W / 2 + trans[0], WINDOW_H / 4 + trans[1])

self.render_road(zoom, trans, angle)
self._render_road(zoom, trans, angle)
self.car.draw(self.surf, zoom, trans, angle, mode != "state_pixels")

self.surf = pygame.transform.flip(self.surf, False, True)

# showing stats
self.render_indicators(WINDOW_W, WINDOW_H)
self._render_indicators(WINDOW_W, WINDOW_H)

font = pygame.font.Font(pygame.font.get_default_font(), 42)
text = font.render("%04i" % self.reward, True, (255, 255, 255), (0, 0, 0))
Expand All @@ -487,19 +514,21 @@ def render(self, mode="human"):
else:
return self.isopen

def render_road(self, zoom, translation, angle):
def _render_road(self, zoom, translation, angle):
bounds = PLAYFIELD
field = [
(2 * bounds, 2 * bounds),
(2 * bounds, 0),
(0, 0),
(0, 2 * bounds),
]
trans_field = []
self.draw_colored_polygon(
self.surf, field, (102, 204, 102), zoom, translation, angle

# draw background
self._draw_colored_polygon(
self.surf, field, self.bg_color, zoom, translation, angle
)

# draw grass patches
k = bounds / (20.0)
grass = []
for x in range(0, 40, 2):
Expand All @@ -513,17 +542,18 @@ def render_road(self, zoom, translation, angle):
]
)
for poly in grass:
self.draw_colored_polygon(
self.surf, poly, (102, 230, 102), zoom, translation, angle
self._draw_colored_polygon(
self.surf, poly, self.grass_color, zoom, translation, angle
)

# draw road
for poly, color in self.road_poly:
# converting to pixel coordinates
poly = [(p[0] + PLAYFIELD, p[1] + PLAYFIELD) for p in poly]
color = [int(c * 255) for c in color]
self.draw_colored_polygon(self.surf, poly, color, zoom, translation, angle)
self._draw_colored_polygon(self.surf, poly, color, zoom, translation, angle)

def render_indicators(self, W, H):
def _render_indicators(self, W, H):
import pygame

s = W / 40.0
Expand Down Expand Up @@ -592,7 +622,7 @@ def render_if_min(value, points, color):
(255, 0, 0),
)

def draw_colored_polygon(self, surface, poly, color, zoom, translation, angle):
def _draw_colored_polygon(self, surface, poly, color, zoom, translation, angle):
import pygame
from pygame import gfxdraw

Expand Down