minigrid.py 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500
  1. import hashlib
  2. import math
  3. from abc import abstractmethod
  4. from enum import IntEnum
  5. from typing import Any, Callable, Optional, Union
  6. import gym
  7. import numpy as np
  8. from gym import spaces
  9. from gym.utils import seeding
  10. # Size in pixels of a tile in the full-scale human view
  11. from gym_minigrid.rendering import (
  12. downsample,
  13. fill_coords,
  14. highlight_img,
  15. point_in_circle,
  16. point_in_line,
  17. point_in_rect,
  18. point_in_triangle,
  19. rotate_fn,
  20. )
  21. from gym_minigrid.window import Window
  22. TILE_PIXELS = 32
  23. # Map of color names to RGB values
  24. COLORS = {
  25. "red": np.array([255, 0, 0]),
  26. "green": np.array([0, 255, 0]),
  27. "blue": np.array([0, 0, 255]),
  28. "purple": np.array([112, 39, 195]),
  29. "yellow": np.array([255, 255, 0]),
  30. "grey": np.array([100, 100, 100]),
  31. }
  32. COLOR_NAMES = sorted(list(COLORS.keys()))
  33. # Used to map colors to integers
  34. COLOR_TO_IDX = {"red": 0, "green": 1, "blue": 2, "purple": 3, "yellow": 4, "grey": 5}
  35. IDX_TO_COLOR = dict(zip(COLOR_TO_IDX.values(), COLOR_TO_IDX.keys()))
  36. # Map of object type to integers
  37. OBJECT_TO_IDX = {
  38. "unseen": 0,
  39. "empty": 1,
  40. "wall": 2,
  41. "floor": 3,
  42. "door": 4,
  43. "key": 5,
  44. "ball": 6,
  45. "box": 7,
  46. "goal": 8,
  47. "lava": 9,
  48. "agent": 10,
  49. }
  50. IDX_TO_OBJECT = dict(zip(OBJECT_TO_IDX.values(), OBJECT_TO_IDX.keys()))
  51. # Map of state names to integers
  52. STATE_TO_IDX = {
  53. "open": 0,
  54. "closed": 1,
  55. "locked": 2,
  56. }
  57. # Map of agent direction indices to vectors
  58. DIR_TO_VEC = [
  59. # Pointing right (positive X)
  60. np.array((1, 0)),
  61. # Down (positive Y)
  62. np.array((0, 1)),
  63. # Pointing left (negative X)
  64. np.array((-1, 0)),
  65. # Up (negative Y)
  66. np.array((0, -1)),
  67. ]
  68. def check_if_no_duplicate(duplicate_list: list) -> bool:
  69. """Check if given list contains any duplicates"""
  70. return len(set(duplicate_list)) == len(duplicate_list)
  71. class MissionSpace(spaces.Space[str]):
  72. r"""A space representing a mission for the Gym-Minigrid environments.
  73. The space allows generating random mission strings constructed with an input placeholder list.
  74. Example Usage::
  75. >>> observation_space = MissionSpace(mission_func=lambda color: "Get the {} ball.".format(color),
  76. ordered_placeholders=[["green", "blue"]])
  77. >>> observation_space.sample()
  78. "Get the green ball."
  79. >>> observation_space = MissionSpace(mission_func=lambda color: "Get the ball.".,
  80. ordered_placeholders=None)
  81. >>> observation_space.sample()
  82. "Get the ball."
  83. """
  84. def __init__(
  85. self,
  86. mission_func: Callable[..., str],
  87. ordered_placeholders: Optional["list[list[str]]"] = None,
  88. seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None,
  89. ):
  90. r"""Constructor of :class:`MissionSpace` space.
  91. Args:
  92. mission_func (lambda _placeholders(str): _mission(str)): Function that generates a mission string from random placeholders.
  93. ordered_placeholders (Optional["list[list[str]]"]): List of lists of placeholders ordered in placing order in the mission function mission_func.
  94. seed: seed: The seed for sampling from the space.
  95. """
  96. # Check that the ordered placeholders and mission function are well defined.
  97. if ordered_placeholders is not None:
  98. assert (
  99. len(ordered_placeholders) == mission_func.__code__.co_argcount
  100. ), "The number of placeholders {} is different from the number of parameters in the mission function {}.".format(
  101. len(ordered_placeholders), mission_func.__code__.co_argcount
  102. )
  103. for placeholder_list in ordered_placeholders:
  104. assert check_if_no_duplicate(
  105. placeholder_list
  106. ), "Make sure that the placeholders don't have any duplicate values."
  107. else:
  108. assert (
  109. mission_func.__code__.co_argcount == 0
  110. ), "If the ordered placeholders are {}, the mission function shouldn't have any parameters.".format(
  111. ordered_placeholders
  112. )
  113. self.ordered_placeholders = ordered_placeholders
  114. self.mission_func = mission_func
  115. super().__init__(dtype=str, seed=seed)
  116. # Check that mission_func returns a string
  117. sampled_mission = self.sample()
  118. assert isinstance(
  119. sampled_mission, str
  120. ), f"mission_func must return type str not {type(sampled_mission)}"
  121. def sample(self) -> str:
  122. """Sample a random mission string."""
  123. if self.ordered_placeholders is not None:
  124. placeholders = []
  125. for rand_var_list in self.ordered_placeholders:
  126. idx = self.np_random.integers(0, len(rand_var_list))
  127. placeholders.append(rand_var_list[idx])
  128. return self.mission_func(*placeholders)
  129. else:
  130. return self.mission_func()
  131. def contains(self, x: Any) -> bool:
  132. """Return boolean specifying if x is a valid member of this space."""
  133. # Store a list of all the placeholders from self.ordered_placeholders that appear in x
  134. if self.ordered_placeholders is not None:
  135. check_placeholder_list = []
  136. for placeholder_list in self.ordered_placeholders:
  137. for placeholder in placeholder_list:
  138. if placeholder in x:
  139. check_placeholder_list.append(placeholder)
  140. # Remove duplicates from the list
  141. check_placeholder_list = list(set(check_placeholder_list))
  142. start_id_placeholder = []
  143. end_id_placeholder = []
  144. # Get the starting and ending id of the identified placeholders with possible duplicates
  145. new_check_placeholder_list = []
  146. for placeholder in check_placeholder_list:
  147. new_start_id_placeholder = [
  148. i for i in range(len(x)) if x.startswith(placeholder, i)
  149. ]
  150. new_check_placeholder_list += [placeholder] * len(
  151. new_start_id_placeholder
  152. )
  153. end_id_placeholder += [
  154. start_id + len(placeholder) - 1
  155. for start_id in new_start_id_placeholder
  156. ]
  157. start_id_placeholder += new_start_id_placeholder
  158. # Order by starting id the placeholders
  159. ordered_placeholder_list = sorted(
  160. zip(
  161. start_id_placeholder, end_id_placeholder, new_check_placeholder_list
  162. )
  163. )
  164. # Check for repeated placeholders contained in each other
  165. remove_placeholder_id = []
  166. for i, placeholder_1 in enumerate(ordered_placeholder_list):
  167. starting_id = i + 1
  168. for j, placeholder_2 in enumerate(
  169. ordered_placeholder_list[starting_id:]
  170. ):
  171. # Check if place holder ids overlap and keep the longest
  172. if max(placeholder_1[0], placeholder_2[0]) < min(
  173. placeholder_1[1], placeholder_2[1]
  174. ):
  175. remove_placeholder = min(
  176. placeholder_1[2], placeholder_2[2], key=len
  177. )
  178. if remove_placeholder == placeholder_1[2]:
  179. remove_placeholder_id.append(i)
  180. else:
  181. remove_placeholder_id.append(i + j + 1)
  182. for id in remove_placeholder_id:
  183. del ordered_placeholder_list[id]
  184. final_placeholders = [
  185. placeholder[2] for placeholder in ordered_placeholder_list
  186. ]
  187. try:
  188. mission_string_with_placeholders = self.mission_func(
  189. *final_placeholders
  190. )
  191. except Exception as e:
  192. print(
  193. f"{x} is not contained in MissionSpace due to the following exception: {e}"
  194. )
  195. return False
  196. return bool(mission_string_with_placeholders == x)
  197. else:
  198. return bool(self.mission_func() == x)
  199. def __repr__(self) -> str:
  200. """Gives a string representation of this space."""
  201. return f"MissionSpace({self.mission_func}, {self.ordered_placeholders})"
  202. def __eq__(self, other) -> bool:
  203. """Check whether ``other`` is equivalent to this instance."""
  204. if isinstance(other, MissionSpace):
  205. # Check that place holder lists are the same
  206. if self.ordered_placeholders is not None:
  207. # Check length
  208. if len(self.order_placeholder) == len(other.order_placeholder):
  209. # Placeholder list are ordered in placing order in the mission string
  210. for placeholder, other_placeholder in zip(
  211. self.order_placeholder, other.order_placeholder
  212. ):
  213. if set(placeholder) == set(other_placeholder):
  214. continue
  215. else:
  216. return False
  217. # Check mission string is the same with dummy space placeholders
  218. test_placeholders = [""] * len(self.order_placeholder)
  219. mission = self.mission_func(*test_placeholders)
  220. other_mission = other.mission_func(*test_placeholders)
  221. return mission == other_mission
  222. else:
  223. # Check that other is also None
  224. if other.ordered_placeholders is None:
  225. # Check mission string is the same
  226. mission = self.mission_func()
  227. other_mission = other.mission_func()
  228. return mission == other_mission
  229. # If none of the statements above return then False
  230. return False
  231. class WorldObj:
  232. """
  233. Base class for grid world objects
  234. """
  235. def __init__(self, type, color):
  236. assert type in OBJECT_TO_IDX, type
  237. assert color in COLOR_TO_IDX, color
  238. self.type = type
  239. self.color = color
  240. self.contains = None
  241. # Initial position of the object
  242. self.init_pos = None
  243. # Current position of the object
  244. self.cur_pos = None
  245. def can_overlap(self):
  246. """Can the agent overlap with this?"""
  247. return False
  248. def can_pickup(self):
  249. """Can the agent pick this up?"""
  250. return False
  251. def can_contain(self):
  252. """Can this contain another object?"""
  253. return False
  254. def see_behind(self):
  255. """Can the agent see behind this object?"""
  256. return True
  257. def toggle(self, env, pos):
  258. """Method to trigger/toggle an action this object performs"""
  259. return False
  260. def encode(self):
  261. """Encode the a description of this object as a 3-tuple of integers"""
  262. return (OBJECT_TO_IDX[self.type], COLOR_TO_IDX[self.color], 0)
  263. @staticmethod
  264. def decode(type_idx, color_idx, state):
  265. """Create an object from a 3-tuple state description"""
  266. obj_type = IDX_TO_OBJECT[type_idx]
  267. color = IDX_TO_COLOR[color_idx]
  268. if obj_type == "empty" or obj_type == "unseen":
  269. return None
  270. # State, 0: open, 1: closed, 2: locked
  271. is_open = state == 0
  272. is_locked = state == 2
  273. if obj_type == "wall":
  274. v = Wall(color)
  275. elif obj_type == "floor":
  276. v = Floor(color)
  277. elif obj_type == "ball":
  278. v = Ball(color)
  279. elif obj_type == "key":
  280. v = Key(color)
  281. elif obj_type == "box":
  282. v = Box(color)
  283. elif obj_type == "door":
  284. v = Door(color, is_open, is_locked)
  285. elif obj_type == "goal":
  286. v = Goal()
  287. elif obj_type == "lava":
  288. v = Lava()
  289. else:
  290. assert False, "unknown object type in decode '%s'" % obj_type
  291. return v
  292. def render(self, r):
  293. """Draw this object with the given renderer"""
  294. raise NotImplementedError
  295. class Goal(WorldObj):
  296. def __init__(self):
  297. super().__init__("goal", "green")
  298. def can_overlap(self):
  299. return True
  300. def render(self, img):
  301. fill_coords(img, point_in_rect(0, 1, 0, 1), COLORS[self.color])
  302. class Floor(WorldObj):
  303. """
  304. Colored floor tile the agent can walk over
  305. """
  306. def __init__(self, color="blue"):
  307. super().__init__("floor", color)
  308. def can_overlap(self):
  309. return True
  310. def render(self, img):
  311. # Give the floor a pale color
  312. color = COLORS[self.color] / 2
  313. fill_coords(img, point_in_rect(0.031, 1, 0.031, 1), color)
  314. class Lava(WorldObj):
  315. def __init__(self):
  316. super().__init__("lava", "red")
  317. def can_overlap(self):
  318. return True
  319. def render(self, img):
  320. c = (255, 128, 0)
  321. # Background color
  322. fill_coords(img, point_in_rect(0, 1, 0, 1), c)
  323. # Little waves
  324. for i in range(3):
  325. ylo = 0.3 + 0.2 * i
  326. yhi = 0.4 + 0.2 * i
  327. fill_coords(img, point_in_line(0.1, ylo, 0.3, yhi, r=0.03), (0, 0, 0))
  328. fill_coords(img, point_in_line(0.3, yhi, 0.5, ylo, r=0.03), (0, 0, 0))
  329. fill_coords(img, point_in_line(0.5, ylo, 0.7, yhi, r=0.03), (0, 0, 0))
  330. fill_coords(img, point_in_line(0.7, yhi, 0.9, ylo, r=0.03), (0, 0, 0))
  331. class Wall(WorldObj):
  332. def __init__(self, color="grey"):
  333. super().__init__("wall", color)
  334. def see_behind(self):
  335. return False
  336. def render(self, img):
  337. fill_coords(img, point_in_rect(0, 1, 0, 1), COLORS[self.color])
  338. class Door(WorldObj):
  339. def __init__(self, color, is_open=False, is_locked=False):
  340. super().__init__("door", color)
  341. self.is_open = is_open
  342. self.is_locked = is_locked
  343. def can_overlap(self):
  344. """The agent can only walk over this cell when the door is open"""
  345. return self.is_open
  346. def see_behind(self):
  347. return self.is_open
  348. def toggle(self, env, pos):
  349. # If the player has the right key to open the door
  350. if self.is_locked:
  351. if isinstance(env.carrying, Key) and env.carrying.color == self.color:
  352. self.is_locked = False
  353. self.is_open = True
  354. return True
  355. return False
  356. self.is_open = not self.is_open
  357. return True
  358. def encode(self):
  359. """Encode the a description of this object as a 3-tuple of integers"""
  360. # State, 0: open, 1: closed, 2: locked
  361. if self.is_open:
  362. state = 0
  363. elif self.is_locked:
  364. state = 2
  365. # if door is closed and unlocked
  366. elif not self.is_open:
  367. state = 1
  368. else:
  369. raise ValueError(
  370. "There is no possible state encoding for the state:\n -Door Open: {}\n -Door Closed: {}\n -Door Locked: {}".format(
  371. self.is_open, not self.is_open, self.is_locked
  372. )
  373. )
  374. return (OBJECT_TO_IDX[self.type], COLOR_TO_IDX[self.color], state)
  375. def render(self, img):
  376. c = COLORS[self.color]
  377. if self.is_open:
  378. fill_coords(img, point_in_rect(0.88, 1.00, 0.00, 1.00), c)
  379. fill_coords(img, point_in_rect(0.92, 0.96, 0.04, 0.96), (0, 0, 0))
  380. return
  381. # Door frame and door
  382. if self.is_locked:
  383. fill_coords(img, point_in_rect(0.00, 1.00, 0.00, 1.00), c)
  384. fill_coords(img, point_in_rect(0.06, 0.94, 0.06, 0.94), 0.45 * np.array(c))
  385. # Draw key slot
  386. fill_coords(img, point_in_rect(0.52, 0.75, 0.50, 0.56), c)
  387. else:
  388. fill_coords(img, point_in_rect(0.00, 1.00, 0.00, 1.00), c)
  389. fill_coords(img, point_in_rect(0.04, 0.96, 0.04, 0.96), (0, 0, 0))
  390. fill_coords(img, point_in_rect(0.08, 0.92, 0.08, 0.92), c)
  391. fill_coords(img, point_in_rect(0.12, 0.88, 0.12, 0.88), (0, 0, 0))
  392. # Draw door handle
  393. fill_coords(img, point_in_circle(cx=0.75, cy=0.50, r=0.08), c)
  394. class Key(WorldObj):
  395. def __init__(self, color="blue"):
  396. super().__init__("key", color)
  397. def can_pickup(self):
  398. return True
  399. def render(self, img):
  400. c = COLORS[self.color]
  401. # Vertical quad
  402. fill_coords(img, point_in_rect(0.50, 0.63, 0.31, 0.88), c)
  403. # Teeth
  404. fill_coords(img, point_in_rect(0.38, 0.50, 0.59, 0.66), c)
  405. fill_coords(img, point_in_rect(0.38, 0.50, 0.81, 0.88), c)
  406. # Ring
  407. fill_coords(img, point_in_circle(cx=0.56, cy=0.28, r=0.190), c)
  408. fill_coords(img, point_in_circle(cx=0.56, cy=0.28, r=0.064), (0, 0, 0))
  409. class Ball(WorldObj):
  410. def __init__(self, color="blue"):
  411. super().__init__("ball", color)
  412. def can_pickup(self):
  413. return True
  414. def render(self, img):
  415. fill_coords(img, point_in_circle(0.5, 0.5, 0.31), COLORS[self.color])
  416. class Box(WorldObj):
  417. def __init__(self, color, contains=None):
  418. super().__init__("box", color)
  419. self.contains = contains
  420. def can_pickup(self):
  421. return True
  422. def render(self, img):
  423. c = COLORS[self.color]
  424. # Outline
  425. fill_coords(img, point_in_rect(0.12, 0.88, 0.12, 0.88), c)
  426. fill_coords(img, point_in_rect(0.18, 0.82, 0.18, 0.82), (0, 0, 0))
  427. # Horizontal slit
  428. fill_coords(img, point_in_rect(0.16, 0.84, 0.47, 0.53), c)
  429. def toggle(self, env, pos):
  430. # Replace the box by its contents
  431. env.grid.set(*pos, self.contains)
  432. return True
  433. class Grid:
  434. """
  435. Represent a grid and operations on it
  436. """
  437. # Static cache of pre-renderer tiles
  438. tile_cache = {}
  439. def __init__(self, width, height):
  440. assert width >= 3
  441. assert height >= 3
  442. self.width = width
  443. self.height = height
  444. self.grid = [None] * width * height
  445. def __contains__(self, key):
  446. if isinstance(key, WorldObj):
  447. for e in self.grid:
  448. if e is key:
  449. return True
  450. elif isinstance(key, tuple):
  451. for e in self.grid:
  452. if e is None:
  453. continue
  454. if (e.color, e.type) == key:
  455. return True
  456. if key[0] is None and key[1] == e.type:
  457. return True
  458. return False
  459. def __eq__(self, other):
  460. grid1 = self.encode()
  461. grid2 = other.encode()
  462. return np.array_equal(grid2, grid1)
  463. def __ne__(self, other):
  464. return not self == other
  465. def copy(self):
  466. from copy import deepcopy
  467. return deepcopy(self)
  468. def set(self, i, j, v):
  469. assert i >= 0 and i < self.width
  470. assert j >= 0 and j < self.height
  471. self.grid[j * self.width + i] = v
  472. def get(self, i, j):
  473. assert i >= 0 and i < self.width
  474. assert j >= 0 and j < self.height
  475. return self.grid[j * self.width + i]
  476. def horz_wall(self, x, y, length=None, obj_type=Wall):
  477. if length is None:
  478. length = self.width - x
  479. for i in range(0, length):
  480. self.set(x + i, y, obj_type())
  481. def vert_wall(self, x, y, length=None, obj_type=Wall):
  482. if length is None:
  483. length = self.height - y
  484. for j in range(0, length):
  485. self.set(x, y + j, obj_type())
  486. def wall_rect(self, x, y, w, h):
  487. self.horz_wall(x, y, w)
  488. self.horz_wall(x, y + h - 1, w)
  489. self.vert_wall(x, y, h)
  490. self.vert_wall(x + w - 1, y, h)
  491. def rotate_left(self):
  492. """
  493. Rotate the grid to the left (counter-clockwise)
  494. """
  495. grid = Grid(self.height, self.width)
  496. for i in range(self.width):
  497. for j in range(self.height):
  498. v = self.get(i, j)
  499. grid.set(j, grid.height - 1 - i, v)
  500. return grid
  501. def slice(self, topX, topY, width, height):
  502. """
  503. Get a subset of the grid
  504. """
  505. grid = Grid(width, height)
  506. for j in range(0, height):
  507. for i in range(0, width):
  508. x = topX + i
  509. y = topY + j
  510. if x >= 0 and x < self.width and y >= 0 and y < self.height:
  511. v = self.get(x, y)
  512. else:
  513. v = Wall()
  514. grid.set(i, j, v)
  515. return grid
  516. @classmethod
  517. def render_tile(
  518. cls, obj, agent_dir=None, highlight=False, tile_size=TILE_PIXELS, subdivs=3
  519. ):
  520. """
  521. Render a tile and cache the result
  522. """
  523. # Hash map lookup key for the cache
  524. key = (agent_dir, highlight, tile_size)
  525. key = obj.encode() + key if obj else key
  526. if key in cls.tile_cache:
  527. return cls.tile_cache[key]
  528. img = np.zeros(
  529. shape=(tile_size * subdivs, tile_size * subdivs, 3), dtype=np.uint8
  530. )
  531. # Draw the grid lines (top and left edges)
  532. fill_coords(img, point_in_rect(0, 0.031, 0, 1), (100, 100, 100))
  533. fill_coords(img, point_in_rect(0, 1, 0, 0.031), (100, 100, 100))
  534. if obj is not None:
  535. obj.render(img)
  536. # Overlay the agent on top
  537. if agent_dir is not None:
  538. tri_fn = point_in_triangle(
  539. (0.12, 0.19),
  540. (0.87, 0.50),
  541. (0.12, 0.81),
  542. )
  543. # Rotate the agent based on its direction
  544. tri_fn = rotate_fn(tri_fn, cx=0.5, cy=0.5, theta=0.5 * math.pi * agent_dir)
  545. fill_coords(img, tri_fn, (255, 0, 0))
  546. # Highlight the cell if needed
  547. if highlight:
  548. highlight_img(img)
  549. # Downsample the image to perform supersampling/anti-aliasing
  550. img = downsample(img, subdivs)
  551. # Cache the rendered tile
  552. cls.tile_cache[key] = img
  553. return img
  554. def render(self, tile_size, agent_pos=None, agent_dir=None, highlight_mask=None):
  555. """
  556. Render this grid at a given scale
  557. :param r: target renderer object
  558. :param tile_size: tile size in pixels
  559. """
  560. if highlight_mask is None:
  561. highlight_mask = np.zeros(shape=(self.width, self.height), dtype=bool)
  562. # Compute the total grid size
  563. width_px = self.width * tile_size
  564. height_px = self.height * tile_size
  565. img = np.zeros(shape=(height_px, width_px, 3), dtype=np.uint8)
  566. # Render the grid
  567. for j in range(0, self.height):
  568. for i in range(0, self.width):
  569. cell = self.get(i, j)
  570. agent_here = np.array_equal(agent_pos, (i, j))
  571. tile_img = Grid.render_tile(
  572. cell,
  573. agent_dir=agent_dir if agent_here else None,
  574. highlight=highlight_mask[i, j],
  575. tile_size=tile_size,
  576. )
  577. ymin = j * tile_size
  578. ymax = (j + 1) * tile_size
  579. xmin = i * tile_size
  580. xmax = (i + 1) * tile_size
  581. img[ymin:ymax, xmin:xmax, :] = tile_img
  582. return img
  583. def encode(self, vis_mask=None):
  584. """
  585. Produce a compact numpy encoding of the grid
  586. """
  587. if vis_mask is None:
  588. vis_mask = np.ones((self.width, self.height), dtype=bool)
  589. array = np.zeros((self.width, self.height, 3), dtype="uint8")
  590. for i in range(self.width):
  591. for j in range(self.height):
  592. if vis_mask[i, j]:
  593. v = self.get(i, j)
  594. if v is None:
  595. array[i, j, 0] = OBJECT_TO_IDX["empty"]
  596. array[i, j, 1] = 0
  597. array[i, j, 2] = 0
  598. else:
  599. array[i, j, :] = v.encode()
  600. return array
  601. @staticmethod
  602. def decode(array):
  603. """
  604. Decode an array grid encoding back into a grid
  605. """
  606. width, height, channels = array.shape
  607. assert channels == 3
  608. vis_mask = np.ones(shape=(width, height), dtype=bool)
  609. grid = Grid(width, height)
  610. for i in range(width):
  611. for j in range(height):
  612. type_idx, color_idx, state = array[i, j]
  613. v = WorldObj.decode(type_idx, color_idx, state)
  614. grid.set(i, j, v)
  615. vis_mask[i, j] = type_idx != OBJECT_TO_IDX["unseen"]
  616. return grid, vis_mask
  617. def process_vis(self, agent_pos):
  618. mask = np.zeros(shape=(self.width, self.height), dtype=bool)
  619. mask[agent_pos[0], agent_pos[1]] = True
  620. for j in reversed(range(0, self.height)):
  621. for i in range(0, self.width - 1):
  622. if not mask[i, j]:
  623. continue
  624. cell = self.get(i, j)
  625. if cell and not cell.see_behind():
  626. continue
  627. mask[i + 1, j] = True
  628. if j > 0:
  629. mask[i + 1, j - 1] = True
  630. mask[i, j - 1] = True
  631. for i in reversed(range(1, self.width)):
  632. if not mask[i, j]:
  633. continue
  634. cell = self.get(i, j)
  635. if cell and not cell.see_behind():
  636. continue
  637. mask[i - 1, j] = True
  638. if j > 0:
  639. mask[i - 1, j - 1] = True
  640. mask[i, j - 1] = True
  641. for j in range(0, self.height):
  642. for i in range(0, self.width):
  643. if not mask[i, j]:
  644. self.set(i, j, None)
  645. return mask
  646. class MiniGridEnv(gym.Env):
  647. """
  648. 2D grid world game environment
  649. """
  650. metadata = {
  651. # Deprecated: use 'render_modes' instead
  652. "render.modes": ["human", "rgb_array"],
  653. "video.frames_per_second": 10, # Deprecated: use 'render_fps' instead
  654. "render_modes": ["human", "rgb_array", "single_rgb_array"],
  655. "render_fps": 10,
  656. }
  657. # Enumeration of possible actions
  658. class Actions(IntEnum):
  659. # Turn left, turn right, move forward
  660. left = 0
  661. right = 1
  662. forward = 2
  663. # Pick up an object
  664. pickup = 3
  665. # Drop an object
  666. drop = 4
  667. # Toggle/activate an object
  668. toggle = 5
  669. # Done completing task
  670. done = 6
  671. def __init__(
  672. self,
  673. mission_space: MissionSpace,
  674. grid_size: int = None,
  675. width: int = None,
  676. height: int = None,
  677. max_steps: int = 100,
  678. see_through_walls: bool = False,
  679. agent_view_size: int = 7,
  680. highlight: bool = True,
  681. tile_size: int = TILE_PIXELS,
  682. **kwargs,
  683. ):
  684. # Initialize mission
  685. self.mission = mission_space.sample()
  686. # Can't set both grid_size and width/height
  687. if grid_size:
  688. assert width is None and height is None
  689. width = grid_size
  690. height = grid_size
  691. # Action enumeration for this environment
  692. self.actions = MiniGridEnv.Actions
  693. # Actions are discrete integer values
  694. self.action_space = spaces.Discrete(len(self.actions))
  695. # Number of cells (width and height) in the agent view
  696. assert agent_view_size % 2 == 1
  697. assert agent_view_size >= 3
  698. self.agent_view_size = agent_view_size
  699. # Observations are dictionaries containing an
  700. # encoding of the grid and a textual 'mission' string
  701. image_observation_space = spaces.Box(
  702. low=0,
  703. high=255,
  704. shape=(self.agent_view_size, self.agent_view_size, 3),
  705. dtype="uint8",
  706. )
  707. self.observation_space = spaces.Dict(
  708. {
  709. "image": image_observation_space,
  710. "direction": spaces.Discrete(4),
  711. "mission": mission_space,
  712. }
  713. )
  714. # Range of possible rewards
  715. self.reward_range = (0, 1)
  716. self.window: Window = None
  717. # Environment configuration
  718. self.width = width
  719. self.height = height
  720. self.max_steps = max_steps
  721. self.see_through_walls = see_through_walls
  722. # Current position and direction of the agent
  723. self.agent_pos: np.ndarray = None
  724. self.agent_dir: int = None
  725. # Initialize the state
  726. self.reset()
  727. def reset(self, *, seed=None, return_info=False, options=None):
  728. super().reset(seed=seed)
  729. # Current position and direction of the agent
  730. self.agent_pos = None
  731. self.agent_dir = None
  732. # Generate a new random grid at the start of each episode
  733. self._gen_grid(self.width, self.height)
  734. # These fields should be defined by _gen_grid
  735. assert self.agent_pos is not None
  736. assert self.agent_dir is not None
  737. # Check that the agent doesn't overlap with an object
  738. start_cell = self.grid.get(*self.agent_pos)
  739. assert start_cell is None or start_cell.can_overlap()
  740. # Item picked up, being carried, initially nothing
  741. self.carrying = None
  742. # Step count since episode start
  743. self.step_count = 0
  744. # Return first observation
  745. obs = self.gen_obs()
  746. if not return_info:
  747. return obs
  748. else:
  749. return obs, {}
  750. def hash(self, size=16):
  751. """Compute a hash that uniquely identifies the current state of the environment.
  752. :param size: Size of the hashing
  753. """
  754. sample_hash = hashlib.sha256()
  755. to_encode = [self.grid.encode().tolist(), self.agent_pos, self.agent_dir]
  756. for item in to_encode:
  757. sample_hash.update(str(item).encode("utf8"))
  758. return sample_hash.hexdigest()[:size]
  759. @property
  760. def steps_remaining(self):
  761. return self.max_steps - self.step_count
  762. def __str__(self):
  763. """
  764. Produce a pretty string of the environment's grid along with the agent.
  765. A grid cell is represented by 2-character string, the first one for
  766. the object and the second one for the color.
  767. """
  768. # Map of object types to short string
  769. OBJECT_TO_STR = {
  770. "wall": "W",
  771. "floor": "F",
  772. "door": "D",
  773. "key": "K",
  774. "ball": "A",
  775. "box": "B",
  776. "goal": "G",
  777. "lava": "V",
  778. }
  779. # Map agent's direction to short string
  780. AGENT_DIR_TO_STR = {0: ">", 1: "V", 2: "<", 3: "^"}
  781. str = ""
  782. for j in range(self.grid.height):
  783. for i in range(self.grid.width):
  784. if i == self.agent_pos[0] and j == self.agent_pos[1]:
  785. str += 2 * AGENT_DIR_TO_STR[self.agent_dir]
  786. continue
  787. c = self.grid.get(i, j)
  788. if c is None:
  789. str += " "
  790. continue
  791. if c.type == "door":
  792. if c.is_open:
  793. str += "__"
  794. elif c.is_locked:
  795. str += "L" + c.color[0].upper()
  796. else:
  797. str += "D" + c.color[0].upper()
  798. continue
  799. str += OBJECT_TO_STR[c.type] + c.color[0].upper()
  800. if j < self.grid.height - 1:
  801. str += "\n"
  802. return str
  803. @abstractmethod
  804. def _gen_grid(self, width, height):
  805. pass
  806. def _reward(self):
  807. """
  808. Compute the reward to be given upon success
  809. """
  810. return 1 - 0.9 * (self.step_count / self.max_steps)
  811. def _rand_int(self, low, high):
  812. """
  813. Generate random integer in [low,high[
  814. """
  815. return self.np_random.integers(low, high)
  816. def _rand_float(self, low, high):
  817. """
  818. Generate random float in [low,high[
  819. """
  820. return self.np_random.uniform(low, high)
  821. def _rand_bool(self):
  822. """
  823. Generate random boolean value
  824. """
  825. return self.np_random.integers(0, 2) == 0
  826. def _rand_elem(self, iterable):
  827. """
  828. Pick a random element in a list
  829. """
  830. lst = list(iterable)
  831. idx = self._rand_int(0, len(lst))
  832. return lst[idx]
  833. def _rand_subset(self, iterable, num_elems):
  834. """
  835. Sample a random subset of distinct elements of a list
  836. """
  837. lst = list(iterable)
  838. assert num_elems <= len(lst)
  839. out = []
  840. while len(out) < num_elems:
  841. elem = self._rand_elem(lst)
  842. lst.remove(elem)
  843. out.append(elem)
  844. return out
  845. def _rand_color(self):
  846. """
  847. Generate a random color name (string)
  848. """
  849. return self._rand_elem(COLOR_NAMES)
  850. def _rand_pos(self, xLow, xHigh, yLow, yHigh):
  851. """
  852. Generate a random (x,y) position tuple
  853. """
  854. return (
  855. self.np_random.integers(xLow, xHigh),
  856. self.np_random.integers(yLow, yHigh),
  857. )
  858. def place_obj(self, obj, top=None, size=None, reject_fn=None, max_tries=math.inf):
  859. """
  860. Place an object at an empty position in the grid
  861. :param top: top-left position of the rectangle where to place
  862. :param size: size of the rectangle where to place
  863. :param reject_fn: function to filter out potential positions
  864. """
  865. if top is None:
  866. top = (0, 0)
  867. else:
  868. top = (max(top[0], 0), max(top[1], 0))
  869. if size is None:
  870. size = (self.grid.width, self.grid.height)
  871. num_tries = 0
  872. while True:
  873. # This is to handle with rare cases where rejection sampling
  874. # gets stuck in an infinite loop
  875. if num_tries > max_tries:
  876. raise RecursionError("rejection sampling failed in place_obj")
  877. num_tries += 1
  878. pos = np.array(
  879. (
  880. self._rand_int(top[0], min(top[0] + size[0], self.grid.width)),
  881. self._rand_int(top[1], min(top[1] + size[1], self.grid.height)),
  882. )
  883. )
  884. # Don't place the object on top of another object
  885. if self.grid.get(*pos) is not None:
  886. continue
  887. # Don't place the object where the agent is
  888. if np.array_equal(pos, self.agent_pos):
  889. continue
  890. # Check if there is a filtering criterion
  891. if reject_fn and reject_fn(self, pos):
  892. continue
  893. break
  894. self.grid.set(*pos, obj)
  895. if obj is not None:
  896. obj.init_pos = pos
  897. obj.cur_pos = pos
  898. return pos
  899. def put_obj(self, obj, i, j):
  900. """
  901. Put an object at a specific position in the grid
  902. """
  903. self.grid.set(i, j, obj)
  904. obj.init_pos = (i, j)
  905. obj.cur_pos = (i, j)
  906. def place_agent(self, top=None, size=None, rand_dir=True, max_tries=math.inf):
  907. """
  908. Set the agent's starting point at an empty position in the grid
  909. """
  910. self.agent_pos = None
  911. pos = self.place_obj(None, top, size, max_tries=max_tries)
  912. self.agent_pos = pos
  913. if rand_dir:
  914. self.agent_dir = self._rand_int(0, 4)
  915. return pos
  916. @property
  917. def dir_vec(self):
  918. """
  919. Get the direction vector for the agent, pointing in the direction
  920. of forward movement.
  921. """
  922. assert self.agent_dir >= 0 and self.agent_dir < 4
  923. return DIR_TO_VEC[self.agent_dir]
  924. @property
  925. def right_vec(self):
  926. """
  927. Get the vector pointing to the right of the agent.
  928. """
  929. dx, dy = self.dir_vec
  930. return np.array((-dy, dx))
  931. @property
  932. def front_pos(self):
  933. """
  934. Get the position of the cell that is right in front of the agent
  935. """
  936. return self.agent_pos + self.dir_vec
  937. def get_view_coords(self, i, j):
  938. """
  939. Translate and rotate absolute grid coordinates (i, j) into the
  940. agent's partially observable view (sub-grid). Note that the resulting
  941. coordinates may be negative or outside of the agent's view size.
  942. """
  943. ax, ay = self.agent_pos
  944. dx, dy = self.dir_vec
  945. rx, ry = self.right_vec
  946. # Compute the absolute coordinates of the top-left view corner
  947. sz = self.agent_view_size
  948. hs = self.agent_view_size // 2
  949. tx = ax + (dx * (sz - 1)) - (rx * hs)
  950. ty = ay + (dy * (sz - 1)) - (ry * hs)
  951. lx = i - tx
  952. ly = j - ty
  953. # Project the coordinates of the object relative to the top-left
  954. # corner onto the agent's own coordinate system
  955. vx = rx * lx + ry * ly
  956. vy = -(dx * lx + dy * ly)
  957. return vx, vy
  958. def get_view_exts(self, agent_view_size=None):
  959. """
  960. Get the extents of the square set of tiles visible to the agent
  961. Note: the bottom extent indices are not included in the set
  962. if agent_view_size is None, use self.agent_view_size
  963. """
  964. agent_view_size = agent_view_size or self.agent_view_size
  965. # Facing right
  966. if self.agent_dir == 0:
  967. topX = self.agent_pos[0]
  968. topY = self.agent_pos[1] - agent_view_size // 2
  969. # Facing down
  970. elif self.agent_dir == 1:
  971. topX = self.agent_pos[0] - agent_view_size // 2
  972. topY = self.agent_pos[1]
  973. # Facing left
  974. elif self.agent_dir == 2:
  975. topX = self.agent_pos[0] - agent_view_size + 1
  976. topY = self.agent_pos[1] - agent_view_size // 2
  977. # Facing up
  978. elif self.agent_dir == 3:
  979. topX = self.agent_pos[0] - agent_view_size // 2
  980. topY = self.agent_pos[1] - agent_view_size + 1
  981. else:
  982. assert False, "invalid agent direction"
  983. botX = topX + agent_view_size
  984. botY = topY + agent_view_size
  985. return (topX, topY, botX, botY)
  986. def relative_coords(self, x, y):
  987. """
  988. Check if a grid position belongs to the agent's field of view, and returns the corresponding coordinates
  989. """
  990. vx, vy = self.get_view_coords(x, y)
  991. if vx < 0 or vy < 0 or vx >= self.agent_view_size or vy >= self.agent_view_size:
  992. return None
  993. return vx, vy
  994. def in_view(self, x, y):
  995. """
  996. check if a grid position is visible to the agent
  997. """
  998. return self.relative_coords(x, y) is not None
  999. def agent_sees(self, x, y):
  1000. """
  1001. Check if a non-empty grid position is visible to the agent
  1002. """
  1003. coordinates = self.relative_coords(x, y)
  1004. if coordinates is None:
  1005. return False
  1006. vx, vy = coordinates
  1007. obs = self.gen_obs()
  1008. obs_grid, _ = Grid.decode(obs["image"])
  1009. obs_cell = obs_grid.get(vx, vy)
  1010. world_cell = self.grid.get(x, y)
  1011. return obs_cell is not None and obs_cell.type == world_cell.type
  1012. def step(self, action):
  1013. self.step_count += 1
  1014. reward = 0
  1015. done = False
  1016. # Get the position in front of the agent
  1017. fwd_pos = self.front_pos
  1018. # Get the contents of the cell in front of the agent
  1019. fwd_cell = self.grid.get(*fwd_pos)
  1020. # Rotate left
  1021. if action == self.actions.left:
  1022. self.agent_dir -= 1
  1023. if self.agent_dir < 0:
  1024. self.agent_dir += 4
  1025. # Rotate right
  1026. elif action == self.actions.right:
  1027. self.agent_dir = (self.agent_dir + 1) % 4
  1028. # Move forward
  1029. elif action == self.actions.forward:
  1030. if fwd_cell is None or fwd_cell.can_overlap():
  1031. self.agent_pos = fwd_pos
  1032. if fwd_cell is not None and fwd_cell.type == "goal":
  1033. done = True
  1034. reward = self._reward()
  1035. if fwd_cell is not None and fwd_cell.type == "lava":
  1036. done = True
  1037. # Pick up an object
  1038. elif action == self.actions.pickup:
  1039. if fwd_cell and fwd_cell.can_pickup():
  1040. if self.carrying is None:
  1041. self.carrying = fwd_cell
  1042. self.carrying.cur_pos = np.array([-1, -1])
  1043. self.grid.set(*fwd_pos, None)
  1044. # Drop an object
  1045. elif action == self.actions.drop:
  1046. if not fwd_cell and self.carrying:
  1047. self.grid.set(*fwd_pos, self.carrying)
  1048. self.carrying.cur_pos = fwd_pos
  1049. self.carrying = None
  1050. # Toggle/activate an object
  1051. elif action == self.actions.toggle:
  1052. if fwd_cell:
  1053. fwd_cell.toggle(self, fwd_pos)
  1054. # Done action (not used by default)
  1055. elif action == self.actions.done:
  1056. pass
  1057. else:
  1058. assert False, "unknown action"
  1059. if self.step_count >= self.max_steps:
  1060. done = True
  1061. obs = self.gen_obs()
  1062. return obs, reward, done, {}
  1063. def gen_obs_grid(self, agent_view_size=None):
  1064. """
  1065. Generate the sub-grid observed by the agent.
  1066. This method also outputs a visibility mask telling us which grid
  1067. cells the agent can actually see.
  1068. if agent_view_size is None, self.agent_view_size is used
  1069. """
  1070. topX, topY, botX, botY = self.get_view_exts(agent_view_size)
  1071. agent_view_size = agent_view_size or self.agent_view_size
  1072. grid = self.grid.slice(topX, topY, agent_view_size, agent_view_size)
  1073. for i in range(self.agent_dir + 1):
  1074. grid = grid.rotate_left()
  1075. # Process occluders and visibility
  1076. # Note that this incurs some performance cost
  1077. if not self.see_through_walls:
  1078. vis_mask = grid.process_vis(
  1079. agent_pos=(agent_view_size // 2, agent_view_size - 1)
  1080. )
  1081. else:
  1082. vis_mask = np.ones(shape=(grid.width, grid.height), dtype=bool)
  1083. # Make it so the agent sees what it's carrying
  1084. # We do this by placing the carried object at the agent's position
  1085. # in the agent's partially observable view
  1086. agent_pos = grid.width // 2, grid.height - 1
  1087. if self.carrying:
  1088. grid.set(*agent_pos, self.carrying)
  1089. else:
  1090. grid.set(*agent_pos, None)
  1091. return grid, vis_mask
  1092. def gen_obs(self):
  1093. """
  1094. Generate the agent's view (partially observable, low-resolution encoding)
  1095. """
  1096. grid, vis_mask = self.gen_obs_grid()
  1097. # Encode the partially observable view into a numpy array
  1098. image = grid.encode(vis_mask)
  1099. assert hasattr(
  1100. self, "mission"
  1101. ), "environments must define a textual mission string"
  1102. # Observations are dictionaries containing:
  1103. # - an image (partially observable view of the environment)
  1104. # - the agent's direction/orientation (acting as a compass)
  1105. # - a textual mission string (instructions for the agent)
  1106. obs = {"image": image, "direction": self.agent_dir, "mission": self.mission}
  1107. return obs
  1108. def get_obs_render(self, obs, tile_size=TILE_PIXELS // 2):
  1109. """
  1110. Render an agent observation for visualization
  1111. """
  1112. grid, vis_mask = Grid.decode(obs)
  1113. # Render the whole grid
  1114. img = grid.render(
  1115. tile_size,
  1116. agent_pos=(self.agent_view_size // 2, self.agent_view_size - 1),
  1117. agent_dir=3,
  1118. highlight_mask=vis_mask,
  1119. )
  1120. return img
  1121. def render(self, mode="human", highlight=True, tile_size=TILE_PIXELS):
  1122. assert mode in self.metadata["render_modes"]
  1123. """
  1124. Render the whole-grid human view
  1125. """
  1126. if mode == "human" and not self.window:
  1127. self.window = Window("gym_minigrid")
  1128. self.window.show(block=False)
  1129. # Compute which cells are visible to the agent
  1130. _, vis_mask = self.gen_obs_grid()
  1131. # Compute the world coordinates of the bottom-left corner
  1132. # of the agent's view area
  1133. f_vec = self.dir_vec
  1134. r_vec = self.right_vec
  1135. top_left = (
  1136. self.agent_pos
  1137. + f_vec * (self.agent_view_size - 1)
  1138. - r_vec * (self.agent_view_size // 2)
  1139. )
  1140. # Mask of which cells to highlight
  1141. highlight_mask = np.zeros(shape=(self.width, self.height), dtype=bool)
  1142. # For each cell in the visibility mask
  1143. for vis_j in range(0, self.agent_view_size):
  1144. for vis_i in range(0, self.agent_view_size):
  1145. # If this cell is not visible, don't highlight it
  1146. if not vis_mask[vis_i, vis_j]:
  1147. continue
  1148. # Compute the world coordinates of this cell
  1149. abs_i, abs_j = top_left - (f_vec * vis_j) + (r_vec * vis_i)
  1150. if abs_i < 0 or abs_i >= self.width:
  1151. continue
  1152. if abs_j < 0 or abs_j >= self.height:
  1153. continue
  1154. # Mark this cell to be highlighted
  1155. highlight_mask[abs_i, abs_j] = True
  1156. # Render the whole grid
  1157. img = self.grid.render(
  1158. tile_size,
  1159. self.agent_pos,
  1160. self.agent_dir,
  1161. highlight_mask=highlight_mask if highlight else None,
  1162. )
  1163. if mode == "human":
  1164. self.window.set_caption(self.mission)
  1165. self.window.show_img(img)
  1166. else:
  1167. return img
  1168. def close(self):
  1169. if self.window:
  1170. self.window.close()