{"id":"own-game-complete","slug":"complete","title":"A complete small game","text":"Here's the coin chase finished: a monster, a time limit and an end. Read it through first. The sections after it explain what's new.\n\n```asset robot.png\n40x40\n```\n\n```asset coin.png\n24x24\n```\n\n```asset monster.png\n40x40\n```\n\n```python\nimport random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.over = True\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x += 1\n        if self.monster_x > self.robot_x:\n            self.monster_x -= 1\n        if self.monster_y < self.robot_y:\n            self.monster_y += 1\n        if self.monster_y > self.robot_y:\n            self.monster_y -= 1\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n```\n\n```window-shot\n{\"image\":\"lessons/windows/325ba617017528d1.png\",\"size\":[480,360],\"title\":\"Coin Chase\",\"frames\":640,\"log\":\"fill (30, 90, 60)\\nblit coin.png at (253, 270)\\nblit robot.png at (35, 85)\\nblit monster.png at (74, 85)\\nblit text \\\"Coins: 3\\\" at (10, 10)\\nblit text \\\"Time: 5\\\" at (390, 10)\\nblit text \\\"Game over! Press R to play again.\\\" at (106, 172)\",\"ended\":\"closed\",\"code\":\"import random\\n\\nimport pygame\\n\\nWIDTH = 480\\nHEIGHT = 360\\nGAME_TIME = 15\\n\\n\\nclass CoinChase:\\n    def __init__(self):\\n        pygame.init()\\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\\n        pygame.display.set_caption(\\\"Coin Chase\\\")\\n        self.robot_picture = pygame.image.load(\\\"robot.png\\\")\\n        self.coin_picture = pygame.image.load(\\\"coin.png\\\")\\n        self.monster_picture = pygame.image.load(\\\"monster.png\\\")\\n        self.font = pygame.font.SysFont(\\\"Arial\\\", 24)\\n        self.clock = pygame.time.Clock()\\n        self.reset()\\n\\n    def reset(self):\\n        self.robot_x = 40\\n        self.robot_y = 160\\n        self.monster_x = 420\\n        self.monster_y = 40\\n        self.coins = 0\\n        self.frames = 0\\n        self.over = False\\n        self.place_coin()\\n\\n    def place_coin(self):\\n        self.coin_x = random.randint(0, WIDTH - 24)\\n        self.coin_y = random.randint(40, HEIGHT - 24)\\n\\n    def seconds_left(self):\\n        return GAME_TIME - self.frames // 60\\n\\n    def run(self):\\n        while True:\\n            self.handle_events()\\n            if not self.over:\\n                self.update()\\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 and event.key == pygame.K_r:\\n                self.reset()\\n\\n    def update(self):\\n        self.move_robot()\\n        self.move_monster()\\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\\n        if robot.colliderect(coin):\\n            self.coins += 1\\n            self.place_coin()\\n        if robot.colliderect(monster):\\n            self.over = True\\n        self.frames += 1\\n        if self.seconds_left() == 0:\\n            self.over = True\\n\\n    def move_robot(self):\\n        keys = pygame.key.get_pressed()\\n        if keys[pygame.K_LEFT]:\\n            self.robot_x -= 3\\n        if keys[pygame.K_RIGHT]:\\n            self.robot_x += 3\\n        if keys[pygame.K_UP]:\\n            self.robot_y -= 3\\n        if keys[pygame.K_DOWN]:\\n            self.robot_y += 3\\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\\n\\n    def move_monster(self):\\n        if self.monster_x < self.robot_x:\\n            self.monster_x += 1\\n        if self.monster_x > self.robot_x:\\n            self.monster_x -= 1\\n        if self.monster_y < self.robot_y:\\n            self.monster_y += 1\\n        if self.monster_y > self.robot_y:\\n            self.monster_y -= 1\\n\\n    def draw(self):\\n        self.window.fill((30, 90, 60))\\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\\n        coins_text = self.font.render(f\\\"Coins: {self.coins}\\\", True, (255, 255, 255))\\n        self.window.blit(coins_text, (10, 10))\\n        time_text = self.font.render(f\\\"Time: {self.seconds_left()}\\\", True, (255, 255, 255))\\n        self.window.blit(time_text, (390, 10))\\n        if self.over:\\n            message = self.font.render(\\\"Game over! Press R to play again.\\\", True, (255, 230, 120))\\n            self.window.blit(message, (106, 172))\\n        pygame.display.flip()\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    game = CoinChase()\\n    game.run()\\n\",\"window\":{\"frames\":640,\"events\":[{\"at\":5,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":55,\"type\":\"keyup\",\"key\":\"K_DOWN\"},{\"at\":55,\"type\":\"keydown\",\"key\":\"K_RIGHT\"},{\"at\":190,\"type\":\"keyup\",\"key\":\"K_RIGHT\"},{\"at\":190,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":265,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":265,\"type\":\"keydown\",\"key\":\"K_LEFT\"},{\"at\":400,\"type\":\"keyup\",\"key\":\"K_LEFT\"}]},\"assets\":[\"robot.png\",\"coin.png\",\"monster.png\"]}\n```\n\n### The monster\n\n`move_monster` moves the monster one pixel closer to the robot on each axis, on every frame:\n\n```python\ndef move_monster(self):\n    if self.monster_x < self.robot_x:\n        self.monster_x += 1\n    if self.monster_x > self.robot_x:\n        self.monster_x -= 1\n    if self.monster_y < self.robot_y:\n        self.monster_y += 1\n    if self.monster_y > self.robot_y:\n        self.monster_y -= 1\n```\n\nThe robot moves 3 pixels a frame and the monster only 1, so a player who keeps moving can stay out of its reach. A game like this is all about such numbers. Try other speeds and see how the game changes.\n\n### Time\n\n`self.frames` counts the frames that the game has run. At 60 frames a second, `self.frames // 60` is the number of whole seconds played, and `seconds_left` subtracts it from the game's length, `GAME_TIME`. A constant at the top of the program makes it easy to try a shorter or a longer game.\n\n### When the game is over\n\nThe attribute `self.over` tells whether the game has ended. `update` sets it to `True` when the monster catches the robot or the time runs out. From then on the main loop skips `update`, so nothing moves, but `draw` keeps showing the last picture with a message on top. R calls `reset`, which sets everything back, `self.over` included.\n\nAn attribute that tells which **phase** a game is in, playing or over, is common in games. A bigger game could have more phases: a title screen, a pause, the next level.\n\n### Make it yours\n\nA game grows one small change at a time. The challenges of this step make three changes to the coin chase: a faster monster, coins that give more time, and lives, so that being caught once doesn't end the game. Some more ideas, for this game or your own:\n\n- more monsters, kept in a list, each moved by the same code\n- a monster that gets faster with every five coins\n- a best score that stays while the program runs, in an attribute that `reset` doesn't touch\n\n### Testing your game\n\nTest the part you're working on without playing the whole game: start the monster right next to the robot to see the ending, or set `GAME_TIME` to 3. Print values while you look for a bug. Then put the real values back.\n\nThen hand the game to someone else, and watch them play without helping. Did they know what to do without being told? Where did they get stuck, or bored? Did they want another round when it ended? That shows you what to work on next. Someone who can read code can also tell you which part of yours was hard to follow.\n\nYou can write and play a game of your own in the Playground of this site, where all its pictures are available.\n","task":"In the challenges of this step the monster is faster. It moves `MONSTER_SPEED` pixels a frame, 2 to begin with. That needs care: a monster 1 pixel to the left of the robot would jump 1 pixel past it, then back again on the next frame, and so on. It would shake.\n\nWrite the method `move_monster`. On every frame, the monster's x moves `MONSTER_SPEED` pixels toward the robot's x, but never past it: if the robot's x is closer than that, the monster's x becomes the robot's x. If the two are already the same, the monster's x stays as it is. The monster's y moves toward the robot's y in the same way.\n","kind":"code","seed":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.over = True\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        # Move the monster toward the robot on both axes, but never past it.\n        pass\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","solution":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.over = True\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x = min(self.monster_x + MONSTER_SPEED, self.robot_x)\n        if self.monster_x > self.robot_x:\n            self.monster_x = max(self.monster_x - MONSTER_SPEED, self.robot_x)\n        if self.monster_y < self.robot_y:\n            self.monster_y = min(self.monster_y + MONSTER_SPEED, self.robot_y)\n        if self.monster_y > self.robot_y:\n            self.monster_y = max(self.monster_y - MONSTER_SPEED, self.robot_y)\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","hints":["Start with x. When the monster's x is smaller than the robot's, it grows by `MONSTER_SPEED`, but not beyond the robot's x. `min(self.monster_x + MONSTER_SPEED, self.robot_x)` is whichever of the two is smaller.","When the monster's x is bigger, it gets smaller by `MONSTER_SPEED`, and `max(...)` with the robot's x stops it at the robot, as `max` kept the robot inside the window.","Use two separate `if` statements for x, so that nothing happens when the two are equal. Then do the same for y."],"tests":[{"name":"The monster comes closer","stdin":"","window":{"frames":120,"events":[]},"expectWindow":{"frames":120,"log":"fill (30, 90, 60)\nblit coin.png at (68, 331)\nblit robot.png at (40, 160)\nblit monster.png at (180, 160)\nblit text \"Coins: 0\" at (10, 10)\nblit text \"Time: 13\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/b2bd4fd7699da281.png"},"expect":"","hidden":false},{"name":"Up, then a little down","stdin":"","window":{"frames":48,"events":[{"at":5,"type":"keydown","key":"K_UP"},{"at":40,"type":"keyup","key":"K_UP"},{"at":40,"type":"keydown","key":"K_DOWN"},{"at":46,"type":"keyup","key":"K_DOWN"}]},"expectWindow":{"frames":48,"log":"fill (30, 90, 60)\nblit coin.png at (68, 331)\nblit robot.png at (40, 73)\nblit monster.png at (324, 73)\nblit text \"Coins: 0\" at (10, 10)\nblit text \"Time: 15\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/50de3bd7552e031a.png"},"expect":"","hidden":true},{"name":"Down, then right under the monster","stdin":"","window":{"frames":117,"events":[{"at":5,"type":"keydown","key":"K_DOWN"},{"at":58,"type":"keyup","key":"K_DOWN"},{"at":58,"type":"keydown","key":"K_RIGHT"},{"at":115,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":117,"log":"fill (30, 90, 60)\nblit coin.png at (433, 72)\nblit robot.png at (211, 319)\nblit monster.png at (211, 274)\nblit text \"Coins: 1\" at (10, 10)\nblit text \"Time: 14\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/016e1b3c5c017010.png"},"expect":"","hidden":true},{"name":"Down, then right toward the monster","stdin":"","window":{"frames":114,"events":[{"at":5,"type":"keydown","key":"K_DOWN"},{"at":62,"type":"keyup","key":"K_DOWN"},{"at":62,"type":"keydown","key":"K_RIGHT"},{"at":113,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":114,"log":"fill (30, 90, 60)\nblit coin.png at (433, 72)\nblit robot.png at (193, 320)\nblit monster.png at (193, 268)\nblit text \"Coins: 1\" at (10, 10)\nblit text \"Time: 14\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/96845f7af0d32732.png"},"expect":"","hidden":true}],"require":[],"forbid":[],"assets":["robot.png","coin.png","monster.png"],"more":[{"task":"The time in the corner stays at 15: it never runs out. Add a time limit, in which every coin gives the robot more time:\n\n- in `update`, add one to `self.frames` on every frame\n- write the method `seconds_left()`. It returns `GAME_TIME`, plus `SECONDS_PER_COIN` seconds for each coin collected (`self.coins`), minus the number of whole seconds played (`self.frames // 60`)\n- at the end of `update`, set `self.over` to `True` when no seconds are left\n","kind":"code","seed":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\nSECONDS_PER_COIN = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        # Return how many whole seconds of the game are left.\n        return GAME_TIME\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x = min(self.monster_x + MONSTER_SPEED, self.robot_x)\n        if self.monster_x > self.robot_x:\n            self.monster_x = max(self.monster_x - MONSTER_SPEED, self.robot_x)\n        if self.monster_y < self.robot_y:\n            self.monster_y = min(self.monster_y + MONSTER_SPEED, self.robot_y)\n        if self.monster_y > self.robot_y:\n            self.monster_y = max(self.monster_y - MONSTER_SPEED, self.robot_y)\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","solution":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\nSECONDS_PER_COIN = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME + SECONDS_PER_COIN * self.coins - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.over = True\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x = min(self.monster_x + MONSTER_SPEED, self.robot_x)\n        if self.monster_x > self.robot_x:\n            self.monster_x = max(self.monster_x - MONSTER_SPEED, self.robot_x)\n        if self.monster_y < self.robot_y:\n            self.monster_y = min(self.monster_y + MONSTER_SPEED, self.robot_y)\n        if self.monster_y > self.robot_y:\n            self.monster_y = max(self.monster_y - MONSTER_SPEED, self.robot_y)\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","hints":["`self.frames` starts at 0 in `reset`. `self.frames += 1` in `update` counts the frames played.","`seconds_left` is one line: start from `GAME_TIME`, add `SECONDS_PER_COIN * self.coins`, and subtract `self.frames // 60`.","After counting the frame: `if self.seconds_left() == 0:` then `self.over = True`. Only ever set it to `True` there, never to `False`: the monster may have ended the game in the same frame."],"tests":[{"name":"A coin gives two more seconds","stdin":"","window":{"frames":100,"events":[{"at":5,"type":"keydown","key":"K_DOWN"},{"at":55,"type":"keyup","key":"K_DOWN"}]},"expectWindow":{"frames":100,"log":"fill (30, 90, 60)\nblit coin.png at (433, 72)\nblit robot.png at (40, 310)\nblit monster.png at (220, 240)\nblit text \"Coins: 1\" at (10, 10)\nblit text \"Time: 16\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/8c7ef3287b135840.png"},"expect":"","hidden":false},{"name":"Seconds left with 2 coins, after 61 frames","stdin":"","call":"game = CoinChase()\ngame.coins = 2\ngame.frames = 61\nprint(game.seconds_left())\n","expect":"18","hidden":false},{"name":"Test 3","stdin":"","window":{"frames":200,"events":[]},"expectWindow":{"frames":200,"log":"fill (30, 90, 60)\nblit coin.png at (68, 331)\nblit robot.png at (40, 160)\nblit monster.png at (78, 160)\nblit text \"Coins: 0\" at (10, 10)\nblit text \"Time: 13\" at (390, 10)\nblit text \"Game over! Press R to play again.\" at (106, 172)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/fe3e067112a442d6.png"},"expect":"","hidden":true},{"name":"Test 4","stdin":"","call":"game = CoinChase()\ngame.frames = 899\ngame.update()\nprint(game.frames, game.over)\n","expect":"900 True","hidden":true},{"name":"Test 5","stdin":"","call":"game = CoinChase()\ngame.coins = 1\ngame.frames = 899\ngame.update()\nprint(game.seconds_left(), game.over)\n","expect":"2 False","hidden":true},{"name":"Test 6","stdin":"","call":"game = CoinChase()\ngame.frames = 500\ngame.update()\nprint(game.seconds_left(), game.over)\n","expect":"7 False","hidden":true}],"require":[],"forbid":[],"assets":["robot.png","coin.png","monster.png"]},{"task":"The monster walks right through the robot, and nothing happens. Give the robot three lives: `reset` already sets `self.lives` to 3, and `draw` shows them. In `update`, when the monster's rectangle and the robot's overlap (both are 40 by 40 pixels):\n\n- the robot loses a life: `self.lives` gets one smaller\n- the monster goes back to where it started, (420, 40)\n- if no lives are left, the game is over: set `self.over` to `True`\n\nWhen the time runs out, the game must still end, as before.\n","kind":"code","seed":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\nSECONDS_PER_COIN = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.lives = 3\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME + SECONDS_PER_COIN * self.coins - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x = min(self.monster_x + MONSTER_SPEED, self.robot_x)\n        if self.monster_x > self.robot_x:\n            self.monster_x = max(self.monster_x - MONSTER_SPEED, self.robot_x)\n        if self.monster_y < self.robot_y:\n            self.monster_y = min(self.monster_y + MONSTER_SPEED, self.robot_y)\n        if self.monster_y > self.robot_y:\n            self.monster_y = max(self.monster_y - MONSTER_SPEED, self.robot_y)\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        lives_text = self.font.render(f\"Lives: {self.lives}\", True, (255, 255, 255))\n        self.window.blit(lives_text, (200, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","solution":"import random\n\nimport pygame\n\nWIDTH = 480\nHEIGHT = 360\nGAME_TIME = 15\nMONSTER_SPEED = 2\nSECONDS_PER_COIN = 2\n\n\nclass CoinChase:\n    def __init__(self):\n        pygame.init()\n        self.window = pygame.display.set_mode((WIDTH, HEIGHT))\n        pygame.display.set_caption(\"Coin Chase\")\n        self.robot_picture = pygame.image.load(\"robot.png\")\n        self.coin_picture = pygame.image.load(\"coin.png\")\n        self.monster_picture = pygame.image.load(\"monster.png\")\n        self.font = pygame.font.SysFont(\"Arial\", 24)\n        self.clock = pygame.time.Clock()\n        self.reset()\n\n    def reset(self):\n        self.robot_x = 40\n        self.robot_y = 160\n        self.monster_x = 420\n        self.monster_y = 40\n        self.coins = 0\n        self.lives = 3\n        self.frames = 0\n        self.over = False\n        self.place_coin()\n\n    def place_coin(self):\n        self.coin_x = random.randint(0, WIDTH - 24)\n        self.coin_y = random.randint(40, HEIGHT - 24)\n\n    def seconds_left(self):\n        return GAME_TIME + SECONDS_PER_COIN * self.coins - self.frames // 60\n\n    def run(self):\n        while True:\n            self.handle_events()\n            if not self.over:\n                self.update()\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 and event.key == pygame.K_r:\n                self.reset()\n\n    def update(self):\n        self.move_robot()\n        self.move_monster()\n        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)\n        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)\n        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)\n        if robot.colliderect(coin):\n            self.coins += 1\n            self.place_coin()\n        if robot.colliderect(monster):\n            self.lives -= 1\n            self.monster_x = 420\n            self.monster_y = 40\n            if self.lives == 0:\n                self.over = True\n        self.frames += 1\n        if self.seconds_left() == 0:\n            self.over = True\n\n    def move_robot(self):\n        keys = pygame.key.get_pressed()\n        if keys[pygame.K_LEFT]:\n            self.robot_x -= 3\n        if keys[pygame.K_RIGHT]:\n            self.robot_x += 3\n        if keys[pygame.K_UP]:\n            self.robot_y -= 3\n        if keys[pygame.K_DOWN]:\n            self.robot_y += 3\n        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))\n        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))\n\n    def move_monster(self):\n        if self.monster_x < self.robot_x:\n            self.monster_x = min(self.monster_x + MONSTER_SPEED, self.robot_x)\n        if self.monster_x > self.robot_x:\n            self.monster_x = max(self.monster_x - MONSTER_SPEED, self.robot_x)\n        if self.monster_y < self.robot_y:\n            self.monster_y = min(self.monster_y + MONSTER_SPEED, self.robot_y)\n        if self.monster_y > self.robot_y:\n            self.monster_y = max(self.monster_y - MONSTER_SPEED, self.robot_y)\n\n    def draw(self):\n        self.window.fill((30, 90, 60))\n        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))\n        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))\n        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))\n        coins_text = self.font.render(f\"Coins: {self.coins}\", True, (255, 255, 255))\n        self.window.blit(coins_text, (10, 10))\n        lives_text = self.font.render(f\"Lives: {self.lives}\", True, (255, 255, 255))\n        self.window.blit(lives_text, (200, 10))\n        time_text = self.font.render(f\"Time: {self.seconds_left()}\", True, (255, 255, 255))\n        self.window.blit(time_text, (390, 10))\n        if self.over:\n            message = self.font.render(\"Game over! Press R to play again.\", True, (255, 230, 120))\n            self.window.blit(message, (106, 172))\n        pygame.display.flip()\n\n\nif __name__ == \"__main__\":\n    game = CoinChase()\n    game.run()\n","hints":["`update` already makes the rectangle `monster`. Check it like the coin: `if robot.colliderect(monster):`.","Inside that `if`, take one from `self.lives`, and set `self.monster_x` and `self.monster_y` back to 420 and 40. Otherwise the monster would still be on the robot in the next frame, and take another life.","Then, still inside it, check whether `self.lives` is 0, and if it is, set `self.over` to `True`. Don't set `self.over` to `False` anywhere in `update`: the time limit may have just ended the game."],"tests":[{"name":"Caught once","stdin":"","window":{"frames":240,"events":[]},"expectWindow":{"frames":240,"log":"fill (30, 90, 60)\nblit coin.png at (68, 331)\nblit robot.png at (40, 160)\nblit monster.png at (282, 160)\nblit text \"Coins: 0\" at (10, 10)\nblit text \"Lives: 2\" at (200, 10)\nblit text \"Time: 11\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/41d309bf4800d1c8.png"},"expect":"","hidden":false},{"name":"Caught three times","stdin":"","window":{"frames":560,"events":[]},"expectWindow":{"frames":560,"log":"fill (30, 90, 60)\nblit coin.png at (68, 331)\nblit robot.png at (40, 160)\nblit monster.png at (420, 40)\nblit text \"Coins: 0\" at (10, 10)\nblit text \"Lives: 0\" at (200, 10)\nblit text \"Time: 7\" at (390, 10)\nblit text \"Game over! Press R to play again.\" at (106, 172)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/6ca33d6ebe24e340.png"},"expect":"","hidden":true},{"name":"Down, then right","stdin":"","window":{"frames":215,"events":[{"at":5,"type":"keydown","key":"K_DOWN"},{"at":60,"type":"keyup","key":"K_DOWN"},{"at":60,"type":"keydown","key":"K_RIGHT"},{"at":120,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":215,"log":"fill (30, 90, 60)\nblit coin.png at (433, 72)\nblit robot.png at (220, 320)\nblit monster.png at (232, 228)\nblit text \"Coins: 1\" at (10, 10)\nblit text \"Lives: 2\" at (200, 10)\nblit text \"Time: 14\" at (390, 10)","size":[480,360],"title":"Coin Chase","image":"lessons/windows/2c80415ad78c3715.png"},"expect":"","hidden":true},{"name":"The time runs out","stdin":"","call":"game = CoinChase()\ngame.frames = 899\ngame.update()\nprint(game.over)\n","expect":"True","hidden":true},{"name":"Caught with the last life","stdin":"","call":"game = CoinChase()\ngame.lives = 1\ngame.monster_x = 70\ngame.monster_y = 160\ngame.update()\nprint(game.lives, game.monster_x, game.monster_y, game.over)\n","expect":"0 420 40 True","hidden":true}],"require":[],"forbid":[],"assets":["robot.png","coin.png","monster.png"]}]}