minigrid_env.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. from __future__ import annotations
  2. import hashlib
  3. import math
  4. from abc import abstractmethod
  5. from typing import Any, Iterable, SupportsFloat, TypeVar
  6. import gymnasium as gym
  7. import numpy as np
  8. import pygame
  9. import pygame.freetype
  10. from gymnasium import spaces
  11. from gymnasium.core import ActType, ObsType
  12. from minigrid.core.actions import Actions
  13. from minigrid.core.constants import COLOR_NAMES, DIR_TO_VEC, TILE_PIXELS
  14. from minigrid.core.grid import Grid
  15. from minigrid.core.mission import MissionSpace
  16. from minigrid.core.world_object import Point, WorldObj
  17. T = TypeVar("T")
  18. class MiniGridEnv(gym.Env):
  19. """
  20. 2D grid world game environment
  21. """
  22. metadata = {
  23. "render_modes": ["human", "rgb_array"],
  24. "render_fps": 10,
  25. }
  26. def __init__(
  27. self,
  28. mission_space: MissionSpace,
  29. grid_size: int | None = None,
  30. width: int | None = None,
  31. height: int | None = None,
  32. max_steps: int = 100,
  33. see_through_walls: bool = False,
  34. agent_view_size: int = 7,
  35. render_mode: str | None = None,
  36. screen_size: int | None = 640,
  37. highlight: bool = True,
  38. tile_size: int = TILE_PIXELS,
  39. agent_pov: bool = False,
  40. ):
  41. # Initialize mission
  42. self.mission = mission_space.sample()
  43. # Can't set both grid_size and width/height
  44. if grid_size:
  45. assert width is None and height is None
  46. width = grid_size
  47. height = grid_size
  48. assert width is not None and height is not None
  49. # Action enumeration for this environment
  50. self.actions = Actions
  51. # Actions are discrete integer values
  52. self.action_space = spaces.Discrete(len(self.actions))
  53. # Number of cells (width and height) in the agent view
  54. assert agent_view_size % 2 == 1
  55. assert agent_view_size >= 3
  56. self.agent_view_size = agent_view_size
  57. # Observations are dictionaries containing an
  58. # encoding of the grid and a textual 'mission' string
  59. image_observation_space = spaces.Box(
  60. low=0,
  61. high=255,
  62. shape=(self.agent_view_size, self.agent_view_size, 3),
  63. dtype="uint8",
  64. )
  65. self.observation_space = spaces.Dict(
  66. {
  67. "image": image_observation_space,
  68. "direction": spaces.Discrete(4),
  69. "mission": mission_space,
  70. }
  71. )
  72. # Range of possible rewards
  73. self.reward_range = (0, 1)
  74. self.screen_size = screen_size
  75. self.render_size = None
  76. self.window = None
  77. self.clock = None
  78. # Environment configuration
  79. self.width = width
  80. self.height = height
  81. assert isinstance(
  82. max_steps, int
  83. ), f"The argument max_steps must be an integer, got: {type(max_steps)}"
  84. self.max_steps = max_steps
  85. self.see_through_walls = see_through_walls
  86. # Current position and direction of the agent
  87. self.agent_pos: np.ndarray | tuple[int, int] = None
  88. self.agent_dir: int = None
  89. # Current grid and mission and carrying
  90. self.grid = Grid(width, height)
  91. self.carrying = None
  92. # Rendering attributes
  93. self.render_mode = render_mode
  94. self.highlight = highlight
  95. self.tile_size = tile_size
  96. self.agent_pov = agent_pov
  97. def reset(
  98. self,
  99. *,
  100. seed: int | None = None,
  101. options: dict[str, Any] | None = None,
  102. ) -> tuple[ObsType, dict[str, Any]]:
  103. super().reset(seed=seed)
  104. # Reinitialize episode-specific variables
  105. self.agent_pos = (-1, -1)
  106. self.agent_dir = -1
  107. # Generate a new random grid at the start of each episode
  108. self._gen_grid(self.width, self.height)
  109. # These fields should be defined by _gen_grid
  110. assert (
  111. self.agent_pos >= (0, 0)
  112. if isinstance(self.agent_pos, tuple)
  113. else all(self.agent_pos >= 0) and self.agent_dir >= 0
  114. )
  115. # Check that the agent doesn't overlap with an object
  116. start_cell = self.grid.get(*self.agent_pos)
  117. assert start_cell is None or start_cell.can_overlap()
  118. # Item picked up, being carried, initially nothing
  119. self.carrying = None
  120. # Step count since episode start
  121. self.step_count = 0
  122. if self.render_mode == "human":
  123. self.render()
  124. # Return first observation
  125. obs = self.gen_obs()
  126. return obs, {}
  127. def hash(self, size=16):
  128. """Compute a hash that uniquely identifies the current state of the environment.
  129. :param size: Size of the hashing
  130. """
  131. sample_hash = hashlib.sha256()
  132. to_encode = [self.grid.encode().tolist(), self.agent_pos, self.agent_dir]
  133. for item in to_encode:
  134. sample_hash.update(str(item).encode("utf8"))
  135. return sample_hash.hexdigest()[:size]
  136. @property
  137. def steps_remaining(self):
  138. return self.max_steps - self.step_count
  139. def pprint_grid(self):
  140. """
  141. Produce a pretty string of the environment's grid along with the agent.
  142. A grid cell is represented by 2-character string, the first one for
  143. the object and the second one for the color.
  144. """
  145. if self.agent_pos is None or self.agent_dir is None or self.grid is None:
  146. raise ValueError(
  147. "The environment hasn't been `reset` therefore the `agent_pos`, `agent_dir` or `grid` are unknown."
  148. )
  149. # Map of object types to short string
  150. OBJECT_TO_STR = {
  151. "wall": "W",
  152. "floor": "F",
  153. "door": "D",
  154. "key": "K",
  155. "ball": "A",
  156. "box": "B",
  157. "goal": "G",
  158. "lava": "V",
  159. }
  160. # Map agent's direction to short string
  161. AGENT_DIR_TO_STR = {0: ">", 1: "V", 2: "<", 3: "^"}
  162. output = ""
  163. # check if self.agent_pos & self.agent_dir is None
  164. # should not be after env is reset
  165. if self.agent_pos is None:
  166. return super().__str__()
  167. for j in range(self.grid.height):
  168. for i in range(self.grid.width):
  169. if i == self.agent_pos[0] and j == self.agent_pos[1]:
  170. output += 2 * AGENT_DIR_TO_STR[self.agent_dir]
  171. continue
  172. tile = self.grid.get(i, j)
  173. if tile is None:
  174. output += " "
  175. continue
  176. if tile.type == "door":
  177. if tile.is_open:
  178. output += "__"
  179. elif tile.is_locked:
  180. output += "L" + tile.color[0].upper()
  181. else:
  182. output += "D" + tile.color[0].upper()
  183. continue
  184. output += OBJECT_TO_STR[tile.type] + tile.color[0].upper()
  185. if j < self.grid.height - 1:
  186. output += "\n"
  187. return output
  188. @abstractmethod
  189. def _gen_grid(self, width, height):
  190. pass
  191. def _reward(self) -> float:
  192. """
  193. Compute the reward to be given upon success
  194. """
  195. return 1 - 0.9 * (self.step_count / self.max_steps)
  196. def _rand_int(self, low: int, high: int) -> int:
  197. """
  198. Generate random integer in [low,high[
  199. """
  200. return self.np_random.integers(low, high)
  201. def _rand_float(self, low: float, high: float) -> float:
  202. """
  203. Generate random float in [low,high[
  204. """
  205. return self.np_random.uniform(low, high)
  206. def _rand_bool(self) -> bool:
  207. """
  208. Generate random boolean value
  209. """
  210. return self.np_random.integers(0, 2) == 0
  211. def _rand_elem(self, iterable: Iterable[T]) -> T:
  212. """
  213. Pick a random element in a list
  214. """
  215. lst = list(iterable)
  216. idx = self._rand_int(0, len(lst))
  217. return lst[idx]
  218. def _rand_subset(self, iterable: Iterable[T], num_elems: int) -> list[T]:
  219. """
  220. Sample a random subset of distinct elements of a list
  221. """
  222. lst = list(iterable)
  223. assert num_elems <= len(lst)
  224. out: list[T] = []
  225. while len(out) < num_elems:
  226. elem = self._rand_elem(lst)
  227. lst.remove(elem)
  228. out.append(elem)
  229. return out
  230. def _rand_color(self) -> str:
  231. """
  232. Generate a random color name (string)
  233. """
  234. return self._rand_elem(COLOR_NAMES)
  235. def _rand_pos(
  236. self, x_low: int, x_high: int, y_low: int, y_high: int
  237. ) -> tuple[int, int]:
  238. """
  239. Generate a random (x,y) position tuple
  240. """
  241. return (
  242. self.np_random.integers(x_low, x_high),
  243. self.np_random.integers(y_low, y_high),
  244. )
  245. def place_obj(
  246. self,
  247. obj: WorldObj | None,
  248. top: Point = None,
  249. size: tuple[int, int] = None,
  250. reject_fn=None,
  251. max_tries=math.inf,
  252. ):
  253. """
  254. Place an object at an empty position in the grid
  255. :param top: top-left position of the rectangle where to place
  256. :param size: size of the rectangle where to place
  257. :param reject_fn: function to filter out potential positions
  258. """
  259. if top is None:
  260. top = (0, 0)
  261. else:
  262. top = (max(top[0], 0), max(top[1], 0))
  263. if size is None:
  264. size = (self.grid.width, self.grid.height)
  265. num_tries = 0
  266. while True:
  267. # This is to handle with rare cases where rejection sampling
  268. # gets stuck in an infinite loop
  269. if num_tries > max_tries:
  270. raise RecursionError("rejection sampling failed in place_obj")
  271. num_tries += 1
  272. pos = (
  273. self._rand_int(top[0], min(top[0] + size[0], self.grid.width)),
  274. self._rand_int(top[1], min(top[1] + size[1], self.grid.height)),
  275. )
  276. # Don't place the object on top of another object
  277. if self.grid.get(*pos) is not None:
  278. continue
  279. # Don't place the object where the agent is
  280. if np.array_equal(pos, self.agent_pos):
  281. continue
  282. # Check if there is a filtering criterion
  283. if reject_fn and reject_fn(self, pos):
  284. continue
  285. break
  286. self.grid.set(pos[0], pos[1], obj)
  287. if obj is not None:
  288. obj.init_pos = pos
  289. obj.cur_pos = pos
  290. return pos
  291. def put_obj(self, obj: WorldObj, i: int, j: int):
  292. """
  293. Put an object at a specific position in the grid
  294. """
  295. self.grid.set(i, j, obj)
  296. obj.init_pos = (i, j)
  297. obj.cur_pos = (i, j)
  298. def place_agent(self, top=None, size=None, rand_dir=True, max_tries=math.inf):
  299. """
  300. Set the agent's starting point at an empty position in the grid
  301. """
  302. self.agent_pos = (-1, -1)
  303. pos = self.place_obj(None, top, size, max_tries=max_tries)
  304. self.agent_pos = pos
  305. if rand_dir:
  306. self.agent_dir = self._rand_int(0, 4)
  307. return pos
  308. @property
  309. def dir_vec(self):
  310. """
  311. Get the direction vector for the agent, pointing in the direction
  312. of forward movement.
  313. """
  314. assert (
  315. self.agent_dir >= 0 and self.agent_dir < 4
  316. ), f"Invalid agent_dir: {self.agent_dir} is not within range(0, 4)"
  317. return DIR_TO_VEC[self.agent_dir]
  318. @property
  319. def right_vec(self):
  320. """
  321. Get the vector pointing to the right of the agent.
  322. """
  323. dx, dy = self.dir_vec
  324. return np.array((-dy, dx))
  325. @property
  326. def front_pos(self):
  327. """
  328. Get the position of the cell that is right in front of the agent
  329. """
  330. return self.agent_pos + self.dir_vec
  331. def get_view_coords(self, i, j):
  332. """
  333. Translate and rotate absolute grid coordinates (i, j) into the
  334. agent's partially observable view (sub-grid). Note that the resulting
  335. coordinates may be negative or outside of the agent's view size.
  336. """
  337. ax, ay = self.agent_pos
  338. dx, dy = self.dir_vec
  339. rx, ry = self.right_vec
  340. # Compute the absolute coordinates of the top-left view corner
  341. sz = self.agent_view_size
  342. hs = self.agent_view_size // 2
  343. tx = ax + (dx * (sz - 1)) - (rx * hs)
  344. ty = ay + (dy * (sz - 1)) - (ry * hs)
  345. lx = i - tx
  346. ly = j - ty
  347. # Project the coordinates of the object relative to the top-left
  348. # corner onto the agent's own coordinate system
  349. vx = rx * lx + ry * ly
  350. vy = -(dx * lx + dy * ly)
  351. return vx, vy
  352. def get_view_exts(self, agent_view_size=None):
  353. """
  354. Get the extents of the square set of tiles visible to the agent
  355. Note: the bottom extent indices are not included in the set
  356. if agent_view_size is None, use self.agent_view_size
  357. """
  358. agent_view_size = agent_view_size or self.agent_view_size
  359. # Facing right
  360. if self.agent_dir == 0:
  361. topX = self.agent_pos[0]
  362. topY = self.agent_pos[1] - agent_view_size // 2
  363. # Facing down
  364. elif self.agent_dir == 1:
  365. topX = self.agent_pos[0] - agent_view_size // 2
  366. topY = self.agent_pos[1]
  367. # Facing left
  368. elif self.agent_dir == 2:
  369. topX = self.agent_pos[0] - agent_view_size + 1
  370. topY = self.agent_pos[1] - agent_view_size // 2
  371. # Facing up
  372. elif self.agent_dir == 3:
  373. topX = self.agent_pos[0] - agent_view_size // 2
  374. topY = self.agent_pos[1] - agent_view_size + 1
  375. else:
  376. assert False, "invalid agent direction"
  377. botX = topX + agent_view_size
  378. botY = topY + agent_view_size
  379. return topX, topY, botX, botY
  380. def relative_coords(self, x, y):
  381. """
  382. Check if a grid position belongs to the agent's field of view, and returns the corresponding coordinates
  383. """
  384. vx, vy = self.get_view_coords(x, y)
  385. if vx < 0 or vy < 0 or vx >= self.agent_view_size or vy >= self.agent_view_size:
  386. return None
  387. return vx, vy
  388. def in_view(self, x, y):
  389. """
  390. check if a grid position is visible to the agent
  391. """
  392. return self.relative_coords(x, y) is not None
  393. def agent_sees(self, x, y):
  394. """
  395. Check if a non-empty grid position is visible to the agent
  396. """
  397. coordinates = self.relative_coords(x, y)
  398. if coordinates is None:
  399. return False
  400. vx, vy = coordinates
  401. obs = self.gen_obs()
  402. obs_grid, _ = Grid.decode(obs["image"])
  403. obs_cell = obs_grid.get(vx, vy)
  404. world_cell = self.grid.get(x, y)
  405. assert world_cell is not None
  406. return obs_cell is not None and obs_cell.type == world_cell.type
  407. def step(
  408. self, action: ActType
  409. ) -> tuple[ObsType, SupportsFloat, bool, bool, dict[str, Any]]:
  410. self.step_count += 1
  411. reward = 0
  412. terminated = False
  413. truncated = False
  414. # Get the position in front of the agent
  415. fwd_pos = self.front_pos
  416. # Get the contents of the cell in front of the agent
  417. fwd_cell = self.grid.get(*fwd_pos)
  418. # Rotate left
  419. if action == self.actions.left:
  420. self.agent_dir -= 1
  421. if self.agent_dir < 0:
  422. self.agent_dir += 4
  423. # Rotate right
  424. elif action == self.actions.right:
  425. self.agent_dir = (self.agent_dir + 1) % 4
  426. # Move forward
  427. elif action == self.actions.forward:
  428. if fwd_cell is None or fwd_cell.can_overlap():
  429. self.agent_pos = tuple(fwd_pos)
  430. if fwd_cell is not None and fwd_cell.type == "goal":
  431. terminated = True
  432. reward = self._reward()
  433. if fwd_cell is not None and fwd_cell.type == "lava":
  434. terminated = True
  435. # Pick up an object
  436. elif action == self.actions.pickup:
  437. if fwd_cell and fwd_cell.can_pickup():
  438. if self.carrying is None:
  439. self.carrying = fwd_cell
  440. self.carrying.cur_pos = np.array([-1, -1])
  441. self.grid.set(fwd_pos[0], fwd_pos[1], None)
  442. # Drop an object
  443. elif action == self.actions.drop:
  444. if not fwd_cell and self.carrying:
  445. self.grid.set(fwd_pos[0], fwd_pos[1], self.carrying)
  446. self.carrying.cur_pos = fwd_pos
  447. self.carrying = None
  448. # Toggle/activate an object
  449. elif action == self.actions.toggle:
  450. if fwd_cell:
  451. fwd_cell.toggle(self, fwd_pos)
  452. # Done action (not used by default)
  453. elif action == self.actions.done:
  454. pass
  455. else:
  456. raise ValueError(f"Unknown action: {action}")
  457. if self.step_count >= self.max_steps:
  458. truncated = True
  459. if self.render_mode == "human":
  460. self.render()
  461. obs = self.gen_obs()
  462. return obs, reward, terminated, truncated, {}
  463. def gen_obs_grid(self, agent_view_size=None):
  464. """
  465. Generate the sub-grid observed by the agent.
  466. This method also outputs a visibility mask telling us which grid
  467. cells the agent can actually see.
  468. if agent_view_size is None, self.agent_view_size is used
  469. """
  470. topX, topY, botX, botY = self.get_view_exts(agent_view_size)
  471. agent_view_size = agent_view_size or self.agent_view_size
  472. grid = self.grid.slice(topX, topY, agent_view_size, agent_view_size)
  473. for i in range(self.agent_dir + 1):
  474. grid = grid.rotate_left()
  475. # Process occluders and visibility
  476. # Note that this incurs some performance cost
  477. if not self.see_through_walls:
  478. vis_mask = grid.process_vis(
  479. agent_pos=(agent_view_size // 2, agent_view_size - 1)
  480. )
  481. else:
  482. vis_mask = np.ones(shape=(grid.width, grid.height), dtype=bool)
  483. # Make it so the agent sees what it's carrying
  484. # We do this by placing the carried object at the agent's position
  485. # in the agent's partially observable view
  486. agent_pos = grid.width // 2, grid.height - 1
  487. if self.carrying:
  488. grid.set(*agent_pos, self.carrying)
  489. else:
  490. grid.set(*agent_pos, None)
  491. return grid, vis_mask
  492. def gen_obs(self):
  493. """
  494. Generate the agent's view (partially observable, low-resolution encoding)
  495. """
  496. grid, vis_mask = self.gen_obs_grid()
  497. # Encode the partially observable view into a numpy array
  498. image = grid.encode(vis_mask)
  499. # Observations are dictionaries containing:
  500. # - an image (partially observable view of the environment)
  501. # - the agent's direction/orientation (acting as a compass)
  502. # - a textual mission string (instructions for the agent)
  503. obs = {"image": image, "direction": self.agent_dir, "mission": self.mission}
  504. return obs
  505. def get_pov_render(self, tile_size):
  506. """
  507. Render an agent's POV observation for visualization
  508. """
  509. grid, vis_mask = self.gen_obs_grid()
  510. # Render the whole grid
  511. img = grid.render(
  512. tile_size,
  513. agent_pos=(self.agent_view_size // 2, self.agent_view_size - 1),
  514. agent_dir=3,
  515. highlight_mask=vis_mask,
  516. )
  517. return img
  518. def get_full_render(self, highlight, tile_size):
  519. """
  520. Render a non-paratial observation for visualization
  521. """
  522. # Compute which cells are visible to the agent
  523. _, vis_mask = self.gen_obs_grid()
  524. # Compute the world coordinates of the bottom-left corner
  525. # of the agent's view area
  526. f_vec = self.dir_vec
  527. r_vec = self.right_vec
  528. top_left = (
  529. self.agent_pos
  530. + f_vec * (self.agent_view_size - 1)
  531. - r_vec * (self.agent_view_size // 2)
  532. )
  533. # Mask of which cells to highlight
  534. highlight_mask = np.zeros(shape=(self.width, self.height), dtype=bool)
  535. # For each cell in the visibility mask
  536. for vis_j in range(0, self.agent_view_size):
  537. for vis_i in range(0, self.agent_view_size):
  538. # If this cell is not visible, don't highlight it
  539. if not vis_mask[vis_i, vis_j]:
  540. continue
  541. # Compute the world coordinates of this cell
  542. abs_i, abs_j = top_left - (f_vec * vis_j) + (r_vec * vis_i)
  543. if abs_i < 0 or abs_i >= self.width:
  544. continue
  545. if abs_j < 0 or abs_j >= self.height:
  546. continue
  547. # Mark this cell to be highlighted
  548. highlight_mask[abs_i, abs_j] = True
  549. # Render the whole grid
  550. img = self.grid.render(
  551. tile_size,
  552. self.agent_pos,
  553. self.agent_dir,
  554. highlight_mask=highlight_mask if highlight else None,
  555. )
  556. return img
  557. def get_frame(
  558. self,
  559. highlight: bool = True,
  560. tile_size: int = TILE_PIXELS,
  561. agent_pov: bool = False,
  562. ):
  563. """Returns an RGB image corresponding to the whole environment or the agent's point of view.
  564. Args:
  565. highlight (bool): If true, the agent's field of view or point of view is highlighted with a lighter gray color.
  566. tile_size (int): How many pixels will form a tile from the NxM grid.
  567. agent_pov (bool): If true, the rendered frame will only contain the point of view of the agent.
  568. Returns:
  569. frame (np.ndarray): A frame of type numpy.ndarray with shape (x, y, 3) representing RGB values for the x-by-y pixel image.
  570. """
  571. if agent_pov:
  572. return self.get_pov_render(tile_size)
  573. else:
  574. return self.get_full_render(highlight, tile_size)
  575. def render(self):
  576. img = self.get_frame(self.highlight, self.tile_size, self.agent_pov)
  577. if self.render_mode == "human":
  578. img = np.transpose(img, axes=(1, 0, 2))
  579. if self.render_size is None:
  580. self.render_size = img.shape[:2]
  581. if self.window is None:
  582. pygame.init()
  583. pygame.display.init()
  584. self.window = pygame.display.set_mode(
  585. (self.screen_size, self.screen_size)
  586. )
  587. pygame.display.set_caption("minigrid")
  588. if self.clock is None:
  589. self.clock = pygame.time.Clock()
  590. surf = pygame.surfarray.make_surface(img)
  591. # Create background with mission description
  592. offset = surf.get_size()[0] * 0.1
  593. # offset = 32 if self.agent_pov else 64
  594. bg = pygame.Surface(
  595. (int(surf.get_size()[0] + offset), int(surf.get_size()[1] + offset))
  596. )
  597. bg.convert()
  598. bg.fill((255, 255, 255))
  599. bg.blit(surf, (offset / 2, 0))
  600. bg = pygame.transform.smoothscale(bg, (self.screen_size, self.screen_size))
  601. font_size = 22
  602. text = self.mission
  603. font = pygame.freetype.SysFont(pygame.font.get_default_font(), font_size)
  604. text_rect = font.get_rect(text, size=font_size)
  605. text_rect.center = bg.get_rect().center
  606. text_rect.y = bg.get_height() - font_size * 1.5
  607. font.render_to(bg, text_rect, text, size=font_size)
  608. self.window.blit(bg, (0, 0))
  609. pygame.event.pump()
  610. self.clock.tick(self.metadata["render_fps"])
  611. pygame.display.flip()
  612. elif self.render_mode == "rgb_array":
  613. return img
  614. def close(self):
  615. if self.window:
  616. pygame.quit()