{"id":"game-project-finishing","slug":"finishing","title":"Finishing the game","text":"The puzzle can be played now, but it isn't a finished game yet. This step adds the finishing touches: a counter of moves, keys for starting over and quitting, and a message when the puzzle is solved.\n\n### A bar for text\n\nThe text goes in a bar under the map, so the window gets one row of tiles taller, and `__init__` makes a font for the text:\n\n```python\nself.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\nself.font = pygame.font.SysFont(\"Arial\", 24)\n```\n\nMake the font once, in `__init__`, and not in `draw`: making a font takes time, and `draw` runs on every frame.\n\n### Counting moves\n\n`reset` sets `self.moves = 0`, and `try_move` adds one at its very end, after all the checks. A bump into a wall returns before it gets there, so it isn't counted. A new method, `draw_bar`, writes the count and the keys in the bar:\n\n```python\ndef draw_bar(self):\n    top = self.height * SIZE + 12\n    text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n    self.window.blit(text, (10, top))\n```\n\nA puzzle player wants to know the count: the same puzzle can be solved in many ways, and the fewer moves, the better.\n\n### Starting over and quitting\n\nPushing a box into a corner ruins the game, so the player needs a way to start over. The R key calls `reset`, which puts the robot, the boxes and the counter back as they were at the start. The Esc key quits, just like closing the window:\n\n```python\nif event.key == pygame.K_r:\n    self.reset()\nif event.key == pygame.K_ESCAPE:\n    exit()\n```\n\nThis works because `reset` makes a brand new list of boxes every time. If the starting places were in one list kept somewhere else, and `reset` said `self.boxes = START_BOXES`, both names would refer to the same list. Every push would change the starting places too, and R would bring back the boxes where they were last, not where they started. `list(START_BOXES)` would make a copy.\n\n### Solved!\n\nThe puzzle is solved when every box is on a target. A method answers the question by going through the boxes. The first box that isn't on a target settles it, so the method can return `False` right there. The answer is `True` only when the loop has checked them all:\n\n```python\ndef solved(self):\n    for x, y in self.boxes:\n        if self.map[y][x] != \"X\":\n            return False\n    return True\n```\n\nWhen the puzzle is solved, `draw_bar` shows a message instead of the count, and `try_move` starts with `if self.solved(): return`, so the robot stays still. The player can still look at the finished puzzle, and press R to play again.\n\n### Testing the ending\n\nTo test the ending, you'd have to solve the whole puzzle every time you change something. While you work on the ending, make it easy: a test level where one push solves it, or, for a moment, a `solved` that just says `return True`. Remember to put things back when the ending works.\n\nHere's the finished game, with all six of its pictures:\n\n```asset floor.png\n40x40\n```\n\n```asset wall.png\n40x40\n```\n\n```asset target.png\n40x40\n```\n\n```asset robot.png\n40x40\n```\n\n```asset box.png\n40x40\n```\n\n```asset box-done.png\n40x40\n```\n\nThe window below shows the puzzle solved in 16 moves. That's the fewest possible. Can you find another way to do it in 16?\n\n```python\nimport pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n```\n\n```window-shot\n{\"image\":\"lessons/windows/a6576db0d3319b34.png\",\"size\":[360,320],\"title\":\"Box Robot\",\"frames\":110,\"log\":\"fill (0, 0, 0)\\nblit wall.png at (0, 0)\\nblit wall.png at (40, 0)\\nblit wall.png at (80, 0)\\nblit wall.png at (120, 0)\\nblit wall.png at (160, 0)\\nblit wall.png at (200, 0)\\nblit wall.png at (240, 0)\\nblit wall.png at (280, 0)\\nblit wall.png at (320, 0)\\nblit wall.png at (0, 40)\\nblit floor.png at (40, 40)\\nblit floor.png at (80, 40)\\nblit floor.png at (120, 40)\\nblit wall.png at (160, 40)\\nblit floor.png at (200, 40)\\nblit floor.png at (240, 40)\\nblit floor.png at (280, 40)\\nblit wall.png at (320, 40)\\nblit wall.png at (0, 80)\\nblit floor.png at (40, 80)\\nblit floor.png at (80, 80)\\nblit floor.png at (120, 80)\\nblit floor.png at (160, 80)\\nblit floor.png at (200, 80)\\nblit target.png at (240, 80)\\nblit box-done.png at (240, 80)\\nblit floor.png at (280, 80)\\nblit wall.png at (320, 80)\\nblit wall.png at (0, 120)\\nblit floor.png at (40, 120)\\nblit floor.png at (80, 120)\\nblit floor.png at (120, 120)\\nblit wall.png at (160, 120)\\nblit floor.png at (200, 120)\\nblit floor.png at (240, 120)\\nblit floor.png at (280, 120)\\nblit wall.png at (320, 120)\\nblit wall.png at (0, 160)\\nblit floor.png at (40, 160)\\nblit floor.png at (80, 160)\\nblit target.png at (120, 160)\\nblit box-done.png at (120, 160)\\nblit wall.png at (160, 160)\\nblit floor.png at (200, 160)\\nblit floor.png at (240, 160)\\nblit floor.png at (280, 160)\\nblit wall.png at (320, 160)\\nblit wall.png at (0, 200)\\nblit floor.png at (40, 200)\\nblit floor.png at (80, 200)\\nblit floor.png at (120, 200)\\nblit floor.png at (160, 200)\\nblit floor.png at (200, 200)\\nblit floor.png at (240, 200)\\nblit floor.png at (280, 200)\\nblit wall.png at (320, 200)\\nblit wall.png at (0, 240)\\nblit wall.png at (40, 240)\\nblit wall.png at (80, 240)\\nblit wall.png at (120, 240)\\nblit wall.png at (160, 240)\\nblit wall.png at (200, 240)\\nblit wall.png at (240, 240)\\nblit wall.png at (280, 240)\\nblit wall.png at (320, 240)\\nblit robot.png at (240, 120)\\nblit text \\\"Solved in 16 moves!\\\" at (10, 292)\\nblit text \\\"R: restart  Esc: quit\\\" at (196, 292)\",\"ended\":\"closed\",\"code\":\"import pygame\\n\\nSIZE = 40\\nLEVEL = [\\n    \\\"#########\\\",\\n    \\\"#...#...#\\\",\\n    \\\"#.....X.#\\\",\\n    \\\"#...#...#\\\",\\n    \\\"#..X#...#\\\",\\n    \\\"#.......#\\\",\\n    \\\"#########\\\",\\n]\\n\\n\\nclass BoxGame:\\n    def __init__(self):\\n        pygame.init()\\n        self.load_pictures()\\n        self.map = LEVEL\\n        self.height = len(self.map)\\n        self.width = len(self.map[0])\\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\\n        pygame.display.set_caption(\\\"Box Robot\\\")\\n        self.font = pygame.font.SysFont(\\\"Arial\\\", 24)\\n        self.clock = pygame.time.Clock()\\n        self.reset()\\n\\n    def load_pictures(self):\\n        self.tiles = {\\n            \\\"#\\\": pygame.image.load(\\\"wall.png\\\"),\\n            \\\".\\\": pygame.image.load(\\\"floor.png\\\"),\\n            \\\"X\\\": pygame.image.load(\\\"target.png\\\"),\\n        }\\n        self.robot_picture = pygame.image.load(\\\"robot.png\\\")\\n        self.box_picture = pygame.image.load(\\\"box.png\\\")\\n        self.done_picture = pygame.image.load(\\\"box-done.png\\\")\\n\\n    def reset(self):\\n        self.robot = (2, 3)\\n        self.boxes = [(2, 2), (6, 4)]\\n        self.moves = 0\\n\\n    def run(self):\\n        while True:\\n            self.handle_events()\\n            self.draw()\\n            self.clock.tick(60)\\n\\n    def handle_events(self):\\n        for event in pygame.event.get():\\n            if event.type == pygame.QUIT:\\n                exit()\\n            if event.type == pygame.KEYDOWN:\\n                if event.key == pygame.K_LEFT:\\n                    self.try_move(-1, 0)\\n                if event.key == pygame.K_RIGHT:\\n                    self.try_move(1, 0)\\n                if event.key == pygame.K_UP:\\n                    self.try_move(0, -1)\\n                if event.key == pygame.K_DOWN:\\n                    self.try_move(0, 1)\\n                if event.key == pygame.K_r:\\n                    self.reset()\\n                if event.key == pygame.K_ESCAPE:\\n                    exit()\\n\\n    def is_free(self, x, y):\\n        return self.map[y][x] != \\\"#\\\" and (x, y) not in self.boxes\\n\\n    def try_move(self, dx, dy):\\n        if self.solved():\\n            return\\n        x, y = self.robot\\n        new_x = x + dx\\n        new_y = y + dy\\n        if self.map[new_y][new_x] == \\\"#\\\":\\n            return\\n        if (new_x, new_y) in self.boxes:\\n            box_x = new_x + dx\\n            box_y = new_y + dy\\n            if not self.is_free(box_x, box_y):\\n                return\\n            self.boxes.remove((new_x, new_y))\\n            self.boxes.append((box_x, box_y))\\n        self.robot = (new_x, new_y)\\n        self.moves += 1\\n\\n    def solved(self):\\n        for x, y in self.boxes:\\n            if self.map[y][x] != \\\"X\\\":\\n                return False\\n        return True\\n\\n    def draw(self):\\n        self.window.fill((0, 0, 0))\\n        for y in range(self.height):\\n            for x in range(self.width):\\n                square = self.map[y][x]\\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\\n                if (x, y) in self.boxes:\\n                    if square == \\\"X\\\":\\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\\n                    else:\\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\\n        robot_x, robot_y = self.robot\\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\\n        self.draw_bar()\\n        pygame.display.flip()\\n\\n    def draw_bar(self):\\n        top = self.height * SIZE + 12\\n        if self.solved():\\n            text = self.font.render(f\\\"Solved in {self.moves} moves!\\\", True, (120, 230, 120))\\n        else:\\n            text = self.font.render(f\\\"Moves: {self.moves}\\\", True, (255, 255, 255))\\n        self.window.blit(text, (10, top))\\n        keys = self.font.render(\\\"R: restart  Esc: quit\\\", True, (160, 160, 160))\\n        self.window.blit(keys, (196, top))\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    game = BoxGame()\\n    game.run()\\n\",\"window\":{\"frames\":110,\"events\":[{\"at\":10,\"type\":\"keydown\",\"key\":\"K_LEFT\"},{\"at\":10,\"type\":\"keyup\",\"key\":\"K_LEFT\"},{\"at\":16,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":16,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":22,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":22,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":28,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":28,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":34,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":34,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":40,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":40,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":46,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":46,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":52,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":52,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":58,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":58,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":64,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":64,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":70,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":70,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":76,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":76,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":82,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":82,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":88,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":88,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":94,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":94,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":100,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":100,\"type\":\"keyup\",\"key\":\"K_UP\"}]},\"assets\":[\"floor.png\",\"wall.png\",\"target.png\",\"robot.png\",\"box.png\",\"box-done.png\"]}\n```\n","task":"The moves aren't counted yet. Fill in the blanks: the counter starts at zero, grows by one with every move the robot makes, and its value is shown in the bar.\n","kind":"fill","seed":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = [[0]]\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        [[self.moves += 1]]\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {[[self.moves]]}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","solution":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","hints":["The counter is the attribute `self.moves`. `reset` gives it the value it starts from, and the f-string in `draw_bar` shows its value between the braces.","The blank in `try_move` is a whole statement that adds one to the counter: `self.moves += 1`. It's at the very end, so a bump into a wall never gets there."],"tests":[{"name":"Two moves and a bump","stdin":"","window":{"frames":20,"events":[{"at":5,"type":"keydown","key":"K_RIGHT"},{"at":5,"type":"keyup","key":"K_RIGHT"},{"at":10,"type":"keydown","key":"K_RIGHT"},{"at":10,"type":"keyup","key":"K_RIGHT"},{"at":15,"type":"keydown","key":"K_UP"},{"at":15,"type":"keyup","key":"K_UP"}]},"expectWindow":{"frames":20,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit box.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (120, 80)\nblit text \"Moves: 2\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/4086457016200538.png"},"expect":"","hidden":false},{"name":"Test 2","stdin":"","window":{"frames":35,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_UP"},{"at":20,"type":"keyup","key":"K_UP"},{"at":25,"type":"keydown","key":"K_RIGHT"},{"at":25,"type":"keyup","key":"K_RIGHT"},{"at":30,"type":"keydown","key":"K_UP"},{"at":30,"type":"keyup","key":"K_UP"}]},"expectWindow":{"frames":35,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit box.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (120, 40)\nblit text \"Moves: 5\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/34c4f29cd844f47a.png"},"expect":"","hidden":true}],"require":[],"forbid":[],"assets":["floor.png","wall.png","target.png","robot.png","box.png","box-done.png"],"more":[{"task":"The bar promises that R restarts the game and Esc quits, but neither key does anything yet. Make them work in `handle_events`: R puts everything back as it was at the start, and Esc ends the program, as closing the window does.\n","kind":"code","seed":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","solution":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","hints":["Both keys come as `KEYDOWN` events, like the arrows. Their names are `pygame.K_r` and `pygame.K_ESCAPE`.","The method `reset` already sets up the robot, the boxes and the counter, so R only needs to call `self.reset()`.","For Esc, call `exit()`, as the code does for `pygame.QUIT`."],"tests":[{"name":"Pushing a box, then R","stdin":"","window":{"frames":30,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_r"},{"at":23,"type":"keyup","key":"K_r"}]},"expectWindow":{"frames":30,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit box.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (80, 120)\nblit text \"Moves: 0\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/01ce3449876f828d.png"},"expect":"","hidden":false},{"name":"Esc","stdin":"","window":{"frames":30,"events":[{"at":3,"type":"keydown","key":"K_RIGHT"},{"at":3,"type":"keyup","key":"K_RIGHT"},{"at":8,"type":"keydown","key":"K_ESCAPE"},{"at":11,"type":"keyup","key":"K_ESCAPE"}]},"expectWindow":{"frames":8,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit box.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (120, 120)\nblit text \"Moves: 1\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/5aa9708a9f9e2ebb.png"},"expect":"","hidden":false},{"name":"Test 3","stdin":"","window":{"frames":40,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_r"},{"at":23,"type":"keyup","key":"K_r"},{"at":25,"type":"keydown","key":"K_UP"},{"at":25,"type":"keyup","key":"K_UP"},{"at":30,"type":"keydown","key":"K_UP"},{"at":30,"type":"keyup","key":"K_UP"}]},"expectWindow":{"frames":40,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit box.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (80, 80)\nblit text \"Moves: 1\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/7199f2da34db3415.png"},"expect":"","hidden":true},{"name":"Test 4","stdin":"","window":{"frames":40,"events":[{"at":4,"type":"keydown","key":"K_UP"},{"at":4,"type":"keyup","key":"K_UP"},{"at":8,"type":"keydown","key":"K_RIGHT"},{"at":8,"type":"keyup","key":"K_RIGHT"},{"at":12,"type":"keydown","key":"K_DOWN"},{"at":12,"type":"keyup","key":"K_DOWN"},{"at":16,"type":"keydown","key":"K_ESCAPE"},{"at":19,"type":"keyup","key":"K_ESCAPE"}]},"expectWindow":{"frames":16,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit box.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (120, 120)\nblit text \"Moves: 3\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/cac79bea9bbb0b2c.png"},"expect":"","hidden":true}],"require":[],"forbid":[],"assets":["floor.png","wall.png","target.png","robot.png","box.png","box-done.png"]},{"task":"Write the method `solved()`. It returns `True` when every box is on a target square, and `False` otherwise. The rest of the game uses it already: once it works, the bar shows a message when the puzzle is solved, and the robot stops.\n","kind":"code","seed":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        # Return True when every box is on a target square.\n        return False\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","solution":"import pygame\n\nSIZE = 40\nLEVEL = [\n    \"#########\",\n    \"#...#...#\",\n    \"#.....X.#\",\n    \"#...#...#\",\n    \"#..X#...#\",\n    \"#.......#\",\n    \"#########\",\n]\n\n\nclass BoxGame:\n    def __init__(self):\n        pygame.init()\n        self.load_pictures()\n        self.map = LEVEL\n        self.height = len(self.map)\n        self.width = len(self.map[0])\n        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE + SIZE))\n        pygame.display.set_caption(\"Box Robot\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def load_pictures(self):\n        self.tiles = {\n            \"#\": pygame.image.load(\"wall.png\"),\n            \".\": pygame.image.load(\"floor.png\"),\n            \"X\": pygame.image.load(\"target.png\"),\n        }\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.box_picture = pygame.image.load(\"box.png\")\n        self.done_picture = pygame.image.load(\"box-done.png\")\n\n    def reset(self):\n        self.robot = (2, 3)\n        self.boxes = [(2, 2), (6, 4)]\n        self.moves = 0\n\n    def run(self):\n        while True:\n            self.handle_events()\n            self.draw()\n            self.clock.tick(60)\n\n    def handle_events(self):\n        for event in pygame.event.get():\n            if event.type == pygame.QUIT:\n                exit()\n            if event.type == pygame.KEYDOWN:\n                if event.key == pygame.K_LEFT:\n                    self.try_move(-1, 0)\n                if event.key == pygame.K_RIGHT:\n                    self.try_move(1, 0)\n                if event.key == pygame.K_UP:\n                    self.try_move(0, -1)\n                if event.key == pygame.K_DOWN:\n                    self.try_move(0, 1)\n                if event.key == pygame.K_r:\n                    self.reset()\n                if event.key == pygame.K_ESCAPE:\n                    exit()\n\n    def is_free(self, x, y):\n        return self.map[y][x] != \"#\" and (x, y) not in self.boxes\n\n    def try_move(self, dx, dy):\n        if self.solved():\n            return\n        x, y = self.robot\n        new_x = x + dx\n        new_y = y + dy\n        if self.map[new_y][new_x] == \"#\":\n            return\n        if (new_x, new_y) in self.boxes:\n            box_x = new_x + dx\n            box_y = new_y + dy\n            if not self.is_free(box_x, box_y):\n                return\n            self.boxes.remove((new_x, new_y))\n            self.boxes.append((box_x, box_y))\n        self.robot = (new_x, new_y)\n        self.moves += 1\n\n    def solved(self):\n        for x, y in self.boxes:\n            if self.map[y][x] != \"X\":\n                return False\n        return True\n\n    def draw(self):\n        self.window.fill((0, 0, 0))\n        for y in range(self.height):\n            for x in range(self.width):\n                square = self.map[y][x]\n                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))\n                if (x, y) in self.boxes:\n                    if square == \"X\":\n                        self.window.blit(self.done_picture, (x * SIZE, y * SIZE))\n                    else:\n                        self.window.blit(self.box_picture, (x * SIZE, y * SIZE))\n        robot_x, robot_y = self.robot\n        self.window.blit(self.robot_picture, (robot_x * SIZE, robot_y * SIZE))\n        self.draw_bar()\n        pygame.display.flip()\n\n    def draw_bar(self):\n        top = self.height * SIZE + 12\n        if self.solved():\n            text = self.font.render(f\"Solved in {self.moves} moves!\", True, (120, 230, 120))\n        else:\n            text = self.font.render(f\"Moves: {self.moves}\", True, (255, 255, 255))\n        self.window.blit(text, (10, top))\n        keys = self.font.render(\"R: restart  Esc: quit\", True, (160, 160, 160))\n        self.window.blit(keys, (196, top))\n\n\nif __name__ == \"__main__\":\n    game = BoxGame()\n    game.run()\n","hints":["Go through the boxes with `for x, y in self.boxes:`, and look at the map under each one.","If the map has anything but `\"X\"` under a box, the puzzle isn't solved: `return False` right away.","Return `True` only after the loop, when no box was off its target."],"tests":[{"name":"Solving the puzzle","stdin":"","window":{"frames":100,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_UP"},{"at":20,"type":"keyup","key":"K_UP"},{"at":25,"type":"keydown","key":"K_RIGHT"},{"at":25,"type":"keyup","key":"K_RIGHT"},{"at":30,"type":"keydown","key":"K_DOWN"},{"at":30,"type":"keyup","key":"K_DOWN"},{"at":35,"type":"keydown","key":"K_DOWN"},{"at":35,"type":"keyup","key":"K_DOWN"},{"at":40,"type":"keydown","key":"K_UP"},{"at":40,"type":"keyup","key":"K_UP"},{"at":45,"type":"keydown","key":"K_RIGHT"},{"at":45,"type":"keyup","key":"K_RIGHT"},{"at":50,"type":"keydown","key":"K_RIGHT"},{"at":50,"type":"keyup","key":"K_RIGHT"},{"at":55,"type":"keydown","key":"K_DOWN"},{"at":55,"type":"keyup","key":"K_DOWN"},{"at":60,"type":"keydown","key":"K_DOWN"},{"at":60,"type":"keyup","key":"K_DOWN"},{"at":65,"type":"keydown","key":"K_DOWN"},{"at":65,"type":"keyup","key":"K_DOWN"},{"at":70,"type":"keydown","key":"K_RIGHT"},{"at":70,"type":"keyup","key":"K_RIGHT"},{"at":75,"type":"keydown","key":"K_UP"},{"at":75,"type":"keyup","key":"K_UP"},{"at":80,"type":"keydown","key":"K_UP"},{"at":80,"type":"keyup","key":"K_UP"}]},"expectWindow":{"frames":100,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit box-done.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit box-done.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (240, 120)\nblit text \"Solved in 16 moves!\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/a6576db0d3319b34.png"},"expect":"","hidden":false},{"name":"Both boxes on targets","stdin":"","call":"game = BoxGame()\ngame.boxes = [(6, 2), (3, 4)]\nprint(game.solved())\n","expect":"True","hidden":false},{"name":"Test 3","stdin":"","call":"game = BoxGame()\nprint(game.solved())\n","expect":"False","hidden":true},{"name":"Test 4","stdin":"","call":"game = BoxGame()\ngame.boxes = [(6, 2), (3, 3)]\nprint(game.solved())\n","expect":"False","hidden":true},{"name":"Test 5","stdin":"","window":{"frames":45,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_UP"},{"at":20,"type":"keyup","key":"K_UP"},{"at":25,"type":"keydown","key":"K_RIGHT"},{"at":25,"type":"keyup","key":"K_RIGHT"},{"at":30,"type":"keydown","key":"K_DOWN"},{"at":30,"type":"keyup","key":"K_DOWN"},{"at":35,"type":"keydown","key":"K_DOWN"},{"at":35,"type":"keyup","key":"K_DOWN"}]},"expectWindow":{"frames":45,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit box-done.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit box.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (120, 120)\nblit text \"Moves: 7\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/0236b9bc39b17b4a.png"},"expect":"","hidden":true},{"name":"Test 6","stdin":"","window":{"frames":110,"events":[{"at":5,"type":"keydown","key":"K_LEFT"},{"at":5,"type":"keyup","key":"K_LEFT"},{"at":10,"type":"keydown","key":"K_UP"},{"at":10,"type":"keyup","key":"K_UP"},{"at":15,"type":"keydown","key":"K_RIGHT"},{"at":15,"type":"keyup","key":"K_RIGHT"},{"at":20,"type":"keydown","key":"K_UP"},{"at":20,"type":"keyup","key":"K_UP"},{"at":25,"type":"keydown","key":"K_RIGHT"},{"at":25,"type":"keyup","key":"K_RIGHT"},{"at":30,"type":"keydown","key":"K_DOWN"},{"at":30,"type":"keyup","key":"K_DOWN"},{"at":35,"type":"keydown","key":"K_DOWN"},{"at":35,"type":"keyup","key":"K_DOWN"},{"at":40,"type":"keydown","key":"K_UP"},{"at":40,"type":"keyup","key":"K_UP"},{"at":45,"type":"keydown","key":"K_RIGHT"},{"at":45,"type":"keyup","key":"K_RIGHT"},{"at":50,"type":"keydown","key":"K_RIGHT"},{"at":50,"type":"keyup","key":"K_RIGHT"},{"at":55,"type":"keydown","key":"K_DOWN"},{"at":55,"type":"keyup","key":"K_DOWN"},{"at":60,"type":"keydown","key":"K_DOWN"},{"at":60,"type":"keyup","key":"K_DOWN"},{"at":65,"type":"keydown","key":"K_DOWN"},{"at":65,"type":"keyup","key":"K_DOWN"},{"at":70,"type":"keydown","key":"K_RIGHT"},{"at":70,"type":"keyup","key":"K_RIGHT"},{"at":75,"type":"keydown","key":"K_UP"},{"at":75,"type":"keyup","key":"K_UP"},{"at":80,"type":"keydown","key":"K_UP"},{"at":80,"type":"keyup","key":"K_UP"},{"at":85,"type":"keydown","key":"K_LEFT"},{"at":85,"type":"keyup","key":"K_LEFT"},{"at":90,"type":"keydown","key":"K_LEFT"},{"at":90,"type":"keyup","key":"K_LEFT"}]},"expectWindow":{"frames":110,"log":"fill (0, 0, 0)\nblit wall.png at (0, 0)\nblit wall.png at (40, 0)\nblit wall.png at (80, 0)\nblit wall.png at (120, 0)\nblit wall.png at (160, 0)\nblit wall.png at (200, 0)\nblit wall.png at (240, 0)\nblit wall.png at (280, 0)\nblit wall.png at (320, 0)\nblit wall.png at (0, 40)\nblit floor.png at (40, 40)\nblit floor.png at (80, 40)\nblit floor.png at (120, 40)\nblit wall.png at (160, 40)\nblit floor.png at (200, 40)\nblit floor.png at (240, 40)\nblit floor.png at (280, 40)\nblit wall.png at (320, 40)\nblit wall.png at (0, 80)\nblit floor.png at (40, 80)\nblit floor.png at (80, 80)\nblit floor.png at (120, 80)\nblit floor.png at (160, 80)\nblit floor.png at (200, 80)\nblit target.png at (240, 80)\nblit box-done.png at (240, 80)\nblit floor.png at (280, 80)\nblit wall.png at (320, 80)\nblit wall.png at (0, 120)\nblit floor.png at (40, 120)\nblit floor.png at (80, 120)\nblit floor.png at (120, 120)\nblit wall.png at (160, 120)\nblit floor.png at (200, 120)\nblit floor.png at (240, 120)\nblit floor.png at (280, 120)\nblit wall.png at (320, 120)\nblit wall.png at (0, 160)\nblit floor.png at (40, 160)\nblit floor.png at (80, 160)\nblit target.png at (120, 160)\nblit box-done.png at (120, 160)\nblit wall.png at (160, 160)\nblit floor.png at (200, 160)\nblit floor.png at (240, 160)\nblit floor.png at (280, 160)\nblit wall.png at (320, 160)\nblit wall.png at (0, 200)\nblit floor.png at (40, 200)\nblit floor.png at (80, 200)\nblit floor.png at (120, 200)\nblit floor.png at (160, 200)\nblit floor.png at (200, 200)\nblit floor.png at (240, 200)\nblit floor.png at (280, 200)\nblit wall.png at (320, 200)\nblit wall.png at (0, 240)\nblit wall.png at (40, 240)\nblit wall.png at (80, 240)\nblit wall.png at (120, 240)\nblit wall.png at (160, 240)\nblit wall.png at (200, 240)\nblit wall.png at (240, 240)\nblit wall.png at (280, 240)\nblit wall.png at (320, 240)\nblit robot.png at (240, 120)\nblit text \"Solved in 16 moves!\" at (10, 292)\nblit text \"R: restart  Esc: quit\" at (196, 292)","size":[360,320],"title":"Box Robot","image":"lessons/windows/a6576db0d3319b34.png"},"expect":"","hidden":true}],"require":[],"forbid":[],"assets":["floor.png","wall.png","target.png","robot.png","box.png","box-done.png"]}]}