{"id":"pygame-techniques-game","slug":"small-game","title":"A small game","text":"Games ask one question all the time: do these two things touch? Does the robot touch the coin, does the monster touch the robot? pygame has a type for the question: **`pygame.Rect`**, a rectangle with a position and a size.\n\n```python\nimport pygame\n\nrobot = pygame.Rect(100, 150, 40, 40)\nprint(robot)\nprint(robot.x, robot.y, robot.width, robot.height)\nprint(robot.right, robot.bottom)\nprint(robot.center)\nrobot.x += 10\nprint(robot.topleft, robot.right)\n```\n\n```output\nRect(100, 150, 40, 40)\n100 150 40 40\n140 190\n(120, 170)\n(110, 150) 150\n```\n\n`right` is `x + width`, the first x past the rectangle (it covers x from 100 to 139), and `bottom` the same for y. `center` is its middle point. Change `x` or `y` and the rectangle moves: `right`, `center` and the rest follow. You can also set them: `robot.center = (160, 120)` moves the rectangle so that its center is there.\n\nAn image gives a rectangle of its own size with `get_rect`: `robot_image.get_rect(topleft=(100, 150))` is the same as `pygame.Rect(100, 150, 40, 40)` for the 40 x 40 robot. And `blit` takes a rectangle as the position, using its top-left corner: `window.blit(robot_image, robot)`.\n\n### Touching\n\n`a.colliderect(b)` tells whether the rectangles `a` and `b` overlap, and `a.collidepoint(point)` whether the point is inside `a`. That's the click test of the previous module, ready-made.\n\n```python\nimport pygame\n\nrobot = pygame.Rect(100, 150, 40, 40)\ncoin = pygame.Rect(130, 130, 24, 24)\nwall = pygame.Rect(140, 150, 40, 40)\nprint(robot.colliderect(coin))\nprint(robot.colliderect(wall))\nprint(robot.collidepoint((120, 160)))\nprint(robot.collidepoint((140, 160)))\n```\n\n```output\nTrue\nFalse\nTrue\nFalse\n```\n\nThe coin's corner overlaps the robot's top-right corner. The wall starts at x = 140, right where the robot ends, so they're side by side but don't overlap.\n\n### Putting it together\n\nHere is a whole game. Coins fly in from the left edge of the window, one at a time, and the player moves the robot up and down with the arrow keys to catch them. Each catch is a point, and the next coin starts from the left. A coin that gets past the robot, all the way out of the window on the right, costs one of the player's three lives, and then the next coin comes. When the last life is gone, the game is over: nothing moves any more, and the window says so until it's closed. A real game would choose each coin's height with `random`; this one takes them in turn from a list.\n\n```asset robot.png\n40x40\n```\n\n```asset coin.png\n24x24\n```\n\n```python\nimport pygame\n\npygame.init()\nwindow = pygame.display.set_mode((320, 240))\npygame.display.set_caption(\"Coin Catcher\")\nrobot_image = pygame.image.load(\"robot.png\")\ncoin_image = pygame.image.load(\"coin.png\")\nfont = pygame.font.SysFont(\"Arial\", 24)\n\nrobot = robot_image.get_rect(topleft=(270, 100))\ncoin_ys = [60, 170, 110, 40, 200]\nnext_y = 0\ncoin = coin_image.get_rect(topleft=(0, coin_ys[0]))\nscore = 0\nlives = 3\ngame_over = False\ngoing_up = False\ngoing_down = False\nclock = pygame.time.Clock()\n\nwhile True:\n    for event in pygame.event.get():\n        if event.type == pygame.KEYDOWN:\n            if event.key == pygame.K_UP:\n                going_up = True\n            if event.key == pygame.K_DOWN:\n                going_down = True\n        if event.type == pygame.KEYUP:\n            if event.key == pygame.K_UP:\n                going_up = False\n            if event.key == pygame.K_DOWN:\n                going_down = False\n        if event.type == pygame.QUIT:\n            exit()\n\n    if not game_over:\n        if going_up and robot.top > 0:\n            robot.y -= 4\n        if going_down and robot.bottom < 240:\n            robot.y += 4\n        coin.x += 5\n        if robot.colliderect(coin):\n            score += 1\n            next_y = (next_y + 1) % len(coin_ys)\n            coin.topleft = (0, coin_ys[next_y])\n        elif coin.left >= 320:\n            lives -= 1\n            if lives == 0:\n                game_over = True\n            else:\n                next_y = (next_y + 1) % len(coin_ys)\n                coin.topleft = (0, coin_ys[next_y])\n\n    window.fill((0, 0, 0))\n    window.blit(robot_image, robot)\n    window.blit(coin_image, coin)\n    score_text = font.render(f\"Score: {score}\", True, (255, 255, 255))\n    window.blit(score_text, (10, 10))\n    lives_text = font.render(f\"Lives: {lives}\", True, (255, 255, 255))\n    window.blit(lives_text, (130, 10))\n    if game_over:\n        text = font.render(\"Game over!\", True, (255, 80, 80))\n        window.blit(text, ((320 - text.get_width()) // 2, 100))\n    pygame.display.flip()\n    clock.tick(60)\n```\n\n```window-shot\n{\"image\":\"lessons/windows/f8506fb9164899c3.png\",\"size\":[320,240],\"title\":\"Coin Catcher\",\"frames\":360,\"log\":\"fill (0, 0, 0)\\nblit robot.png at (270, 168)\\nblit coin.png at (320, 60)\\nblit text \\\"Score: 3\\\" at (10, 10)\\nblit text \\\"Lives: 0\\\" at (130, 10)\\nblit text \\\"Game over!\\\" at (115, 100)\",\"ended\":\"closed\",\"code\":\"import pygame\\n\\npygame.init()\\nwindow = pygame.display.set_mode((320, 240))\\npygame.display.set_caption(\\\"Coin Catcher\\\")\\nrobot_image = pygame.image.load(\\\"robot.png\\\")\\ncoin_image = pygame.image.load(\\\"coin.png\\\")\\nfont = pygame.font.SysFont(\\\"Arial\\\", 24)\\n\\nrobot = robot_image.get_rect(topleft=(270, 100))\\ncoin_ys = [60, 170, 110, 40, 200]\\nnext_y = 0\\ncoin = coin_image.get_rect(topleft=(0, coin_ys[0]))\\nscore = 0\\nlives = 3\\ngame_over = False\\ngoing_up = False\\ngoing_down = False\\nclock = pygame.time.Clock()\\n\\nwhile True:\\n    for event in pygame.event.get():\\n        if event.type == pygame.KEYDOWN:\\n            if event.key == pygame.K_UP:\\n                going_up = True\\n            if event.key == pygame.K_DOWN:\\n                going_down = True\\n        if event.type == pygame.KEYUP:\\n            if event.key == pygame.K_UP:\\n                going_up = False\\n            if event.key == pygame.K_DOWN:\\n                going_down = False\\n        if event.type == pygame.QUIT:\\n            exit()\\n\\n    if not game_over:\\n        if going_up and robot.top > 0:\\n            robot.y -= 4\\n        if going_down and robot.bottom < 240:\\n            robot.y += 4\\n        coin.x += 5\\n        if robot.colliderect(coin):\\n            score += 1\\n            next_y = (next_y + 1) % len(coin_ys)\\n            coin.topleft = (0, coin_ys[next_y])\\n        elif coin.left >= 320:\\n            lives -= 1\\n            if lives == 0:\\n                game_over = True\\n            else:\\n                next_y = (next_y + 1) % len(coin_ys)\\n                coin.topleft = (0, coin_ys[next_y])\\n\\n    window.fill((0, 0, 0))\\n    window.blit(robot_image, robot)\\n    window.blit(coin_image, coin)\\n    score_text = font.render(f\\\"Score: {score}\\\", True, (255, 255, 255))\\n    window.blit(score_text, (10, 10))\\n    lives_text = font.render(f\\\"Lives: {lives}\\\", True, (255, 255, 255))\\n    window.blit(lives_text, (130, 10))\\n    if game_over:\\n        text = font.render(\\\"Game over!\\\", True, (255, 80, 80))\\n        window.blit(text, ((320 - text.get_width()) // 2, 100))\\n    pygame.display.flip()\\n    clock.tick(60)\\n\",\"window\":{\"frames\":360,\"events\":[{\"at\":10,\"type\":\"keydown\",\"key\":\"K_UP\"},{\"at\":18,\"type\":\"keyup\",\"key\":\"K_UP\"},{\"at\":60,\"type\":\"keydown\",\"key\":\"K_DOWN\"},{\"at\":85,\"type\":\"keyup\",\"key\":\"K_DOWN\"}]},\"assets\":[\"robot.png\",\"coin.png\"]}\n```\n\nThe game is the same main loop as before, with its three parts:\n\n1. **Events** only change variables: the arrow keys set `going_up` and `going_down`.\n2. **Updating** applies the game's rules to its **state**, the variables that describe the game: the robot moves (but not out of the window), the coin flies on, a catch adds a point and starts the next coin, and a missed coin takes a life. It then starts the next coin, or, when that was the last life, sets `game_over`. When `game_over` is `True`, this part does nothing, so everything stays where it was.\n3. **Drawing** shows the state as it is now, including the score, the lives and, at the end, the words \"Game over!\".\n\nKeeping the parts apart makes a game much easier to change. A new rule goes in the updating part, and the drawing part draws whatever the state is.\n","task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"import pygame\n\nplayer = pygame.Rect(100, 150, 40, 40)\nwall = pygame.Rect(130, 100, 50, 60)\nprint(player.right, player.bottom)\nprint(player.center)\nprint(player.colliderect(wall))\nplayer.x -= 20\nprint(player.colliderect(wall))\n","solution":"import pygame\n\nplayer = pygame.Rect(100, 150, 40, 40)\nwall = pygame.Rect(130, 100, 50, 60)\nprint(player.right, player.bottom)\nprint(player.center)\nprint(player.colliderect(wall))\nplayer.x -= 20\nprint(player.colliderect(wall))\n","hints":["`right` is x + width and `bottom` is y + height. The center is half the size from the top-left corner.","The wall covers x from 130 to 179 and y from 100 to 159. The player covers x from 100 to 139 and y from 150 to 189.","After moving 20 to the left, the player covers x from 80 to 119, which doesn't reach the wall's 130."],"tests":[{"name":"Output","stdin":"","expect":"140 190\n(120, 170)\nTrue\nFalse","hidden":false}],"require":[],"forbid":[],"lines":["140 190","(120, 170)","True","False"],"more":[{"task":"Collect the coins with clicks. Each coin is a `pygame.Rect`, and a click on a coin removes it from the list. The text at the bottom tells how many coins are left. Fill in the blanks: the rectangle method that tells whether a point is inside, and the function that gives the length of the list.\n","kind":"fill","seed":"import pygame\n\npygame.init()\nwindow = pygame.display.set_mode((320, 240))\ncoin = pygame.image.load(\"coin.png\")\nfont = pygame.font.SysFont(\"Arial\", 24)\n\ncoins = [pygame.Rect(40, 40, 24, 24), pygame.Rect(150, 100, 24, 24), pygame.Rect(260, 60, 24, 24)]\n\nwhile True:\n    for event in pygame.event.get():\n        if event.type == pygame.MOUSEBUTTONDOWN:\n            for coin_rect in coins:\n                if coin_rect.[[collidepoint]](event.pos):\n                    coins.remove(coin_rect)\n                    break\n        if event.type == pygame.QUIT:\n            exit()\n\n    window.fill((0, 0, 0))\n    for coin_rect in coins:\n        window.blit(coin, coin_rect)\n    text = font.render(f\"Coins left: {[[len]](coins)}\", True, (255, 255, 255))\n    window.blit(text, (10, 200))\n    pygame.display.flip()\n","solution":"import pygame\n\npygame.init()\nwindow = pygame.display.set_mode((320, 240))\ncoin = pygame.image.load(\"coin.png\")\nfont = pygame.font.SysFont(\"Arial\", 24)\n\ncoins = [pygame.Rect(40, 40, 24, 24), pygame.Rect(150, 100, 24, 24), pygame.Rect(260, 60, 24, 24)]\n\nwhile True:\n    for event in pygame.event.get():\n        if event.type == pygame.MOUSEBUTTONDOWN:\n            for coin_rect in coins:\n                if coin_rect.collidepoint(event.pos):\n                    coins.remove(coin_rect)\n                    break\n        if event.type == pygame.QUIT:\n            exit()\n\n    window.fill((0, 0, 0))\n    for coin_rect in coins:\n        window.blit(coin, coin_rect)\n    text = font.render(f\"Coins left: {len(coins)}\", True, (255, 255, 255))\n    window.blit(text, (10, 200))\n    pygame.display.flip()\n","hints":["One method of a rectangle checks a point, the other checks another rectangle. Which one is for a point?","`coin_rect.collidepoint(event.pos)`, and the length of a list is `len(coins)`."],"tests":[{"name":"Two coins collected","stdin":"","window":{"frames":20,"events":[{"at":3,"type":"mousedown","pos":[50,50],"button":1},{"at":4,"type":"mouseup","pos":[50,50],"button":1},{"at":8,"type":"mousedown","pos":[10,150],"button":1},{"at":9,"type":"mouseup","pos":[10,150],"button":1},{"at":12,"type":"mousedown","pos":[270,80],"button":1},{"at":13,"type":"mouseup","pos":[270,80],"button":1}]},"expectWindow":{"frames":20,"log":"fill (0, 0, 0)\nblit coin.png at (150, 100)\nblit text \"Coins left: 1\" at (10, 200)","size":[320,240],"title":"pygame window","image":"lessons/windows/de437150794e2633.png"},"expect":"","hidden":false},{"name":"Test 2","stdin":"","window":{"frames":20,"events":[{"at":2,"type":"mousedown","pos":[160,110],"button":1},{"at":3,"type":"mouseup","pos":[160,110],"button":1},{"at":6,"type":"mousedown","pos":[64,64],"button":1},{"at":7,"type":"mouseup","pos":[64,64],"button":1}]},"expectWindow":{"frames":20,"log":"fill (0, 0, 0)\nblit coin.png at (40, 40)\nblit coin.png at (260, 60)\nblit text \"Coins left: 2\" at (10, 200)","size":[320,240],"title":"pygame window","image":"lessons/windows/f12c362ea1c45bd2.png"},"expect":"","hidden":true}],"require":[],"forbid":[],"assets":["coin.png"]},{"task":"Write a dodging game. The robot stands at the bottom of the window, at (140, 200), and a monster falls toward it. The player moves the robot with the arrow keys, 4 pixels per frame while the key is held, and the robot's x stays between 0 and 280.\n\nThe monster starts at the top (y = 0) and falls 5 pixels per frame. Its x comes from the list `[40, 200, 120, 260, 0]`: the first monster falls at x = 40, the next one at 200, and so on, back to the start of the list after the last one. When a monster's y reaches 240 or more, it has been dodged: the count of dodged monsters grows by one, and the next monster starts at the top.\n\nWhen the robot and the monster touch (their rectangles overlap), the game is over: from then on nothing moves, and the window shows `Game over!` too, until it's closed.\n\nIn each frame: handle the events, then (unless the game is over) move the robot, move the monster, check for a dodged monster and check for touching. Then draw: fill the window black, draw the robot, the monster, the text `Dodged: 2` (with the real count) at (10, 10), and, when the game is over, the text `Game over!` centered across the window with its top at y = 100. Use the starter code's font for both texts.\n","kind":"code","seed":"import pygame\n\npygame.init()\nwindow = pygame.display.set_mode((320, 240))\npygame.display.set_caption(\"Dodge\")\nrobot_image = pygame.image.load(\"robot.png\")\nmonster_image = pygame.image.load(\"monster.png\")\nfont = pygame.font.SysFont(\"Arial\", 24)\n\nrobot = robot_image.get_rect(topleft=(140, 200))\nmonster_xs = [40, 200, 120, 260, 0]\nclock = pygame.time.Clock()\n\nwhile True:\n    for event in pygame.event.get():\n        if event.type == pygame.QUIT:\n            exit()\n\n    window.fill((0, 0, 0))\n    window.blit(robot_image, robot)\n    pygame.display.flip()\n    clock.tick(60)\n","solution":"import pygame\n\npygame.init()\nwindow = pygame.display.set_mode((320, 240))\npygame.display.set_caption(\"Dodge\")\nrobot_image = pygame.image.load(\"robot.png\")\nmonster_image = pygame.image.load(\"monster.png\")\nfont = pygame.font.SysFont(\"Arial\", 24)\n\nrobot = robot_image.get_rect(topleft=(140, 200))\nmonster_xs = [40, 200, 120, 260, 0]\nnext_x = 0\nmonster = monster_image.get_rect(topleft=(monster_xs[0], 0))\ndodged = 0\ngame_over = False\ngoing_left = False\ngoing_right = False\nclock = pygame.time.Clock()\n\nwhile True:\n    for event in pygame.event.get():\n        if event.type == pygame.KEYDOWN:\n            if event.key == pygame.K_LEFT:\n                going_left = True\n            if event.key == pygame.K_RIGHT:\n                going_right = True\n        if event.type == pygame.KEYUP:\n            if event.key == pygame.K_LEFT:\n                going_left = False\n            if event.key == pygame.K_RIGHT:\n                going_right = False\n        if event.type == pygame.QUIT:\n            exit()\n\n    if not game_over:\n        if going_left:\n            robot.x -= 4\n        if going_right:\n            robot.x += 4\n        if robot.x < 0:\n            robot.x = 0\n        if robot.x > 280:\n            robot.x = 280\n        monster.y += 5\n        if monster.y >= 240:\n            dodged += 1\n            next_x = (next_x + 1) % len(monster_xs)\n            monster.topleft = (monster_xs[next_x], 0)\n        if robot.colliderect(monster):\n            game_over = True\n\n    window.fill((0, 0, 0))\n    window.blit(robot_image, robot)\n    window.blit(monster_image, monster)\n    text = font.render(f\"Dodged: {dodged}\", True, (255, 255, 255))\n    window.blit(text, (10, 10))\n    if game_over:\n        over = font.render(\"Game over!\", True, (255, 80, 80))\n        window.blit(over, ((320 - over.get_width()) // 2, 100))\n    pygame.display.flip()\n    clock.tick(60)\n","hints":["Start from the lesson's game: variables for the arrow keys (here left and right), a `pygame.Rect` for the monster made with `monster_image.get_rect(topleft=(monster_xs[0], 0))`, a count and `game_over = False`.","In the updating part, under `if not game_over:`, move and clamp the robot, then `monster.y += 5`. When `monster.y >= 240`, count it and set `monster.topleft` to the next x of the list and y 0.","Then `if robot.colliderect(monster):` ends the game. Draw the `Game over!` text only when `game_over` is `True`, centered with its own `get_width()` or with a rectangle: `over.get_rect(midtop=(160, 100))`."],"tests":[{"name":"Standing still","stdin":"","window":{"frames":150,"events":[]},"expectWindow":{"frames":150,"log":"fill (0, 0, 0)\nblit robot.png at (140, 200)\nblit monster.png at (120, 165)\nblit text \"Dodged: 2\" at (10, 10)\nblit text \"Game over!\" at (115, 100)","size":[320,240],"title":"Dodge","image":"lessons/windows/a1f5e9a80e2b47f6.png"},"expect":"","hidden":false},{"name":"Dodging to the right","stdin":"","window":{"frames":240,"events":[{"at":100,"type":"keydown","key":"K_RIGHT"},{"at":130,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":240,"log":"fill (0, 0, 0)\nblit robot.png at (260, 200)\nblit monster.png at (260, 165)\nblit text \"Dodged: 3\" at (10, 10)\nblit text \"Game over!\" at (115, 100)","size":[320,240],"title":"Dodge","image":"lessons/windows/1de6aa0abd597519.png"},"expect":"","hidden":true},{"name":"A long game","stdin":"","window":{"frames":300,"events":[{"at":100,"type":"keydown","key":"K_LEFT"},{"at":120,"type":"keyup","key":"K_LEFT"},{"at":250,"type":"keydown","key":"K_RIGHT"},{"at":262,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":300,"log":"fill (0, 0, 0)\nblit robot.png at (108, 200)\nblit monster.png at (200, 60)\nblit text \"Dodged: 6\" at (10, 10)","size":[320,240],"title":"Dodge","image":"lessons/windows/57d02867453d124b.png"},"expect":"","hidden":true},{"name":"In the middle of the game","stdin":"","window":{"frames":70,"events":[{"at":10,"type":"keydown","key":"K_RIGHT"},{"at":30,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":70,"log":"fill (0, 0, 0)\nblit robot.png at (220, 200)\nblit monster.png at (200, 110)\nblit text \"Dodged: 1\" at (10, 10)","size":[320,240],"title":"Dodge","image":"lessons/windows/b3f1b6ed452236a0.png"},"expect":"","hidden":true},{"name":"Into the wall","stdin":"","window":{"frames":60,"events":[{"at":3,"type":"keydown","key":"K_RIGHT"},{"at":55,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":60,"log":"fill (0, 0, 0)\nblit robot.png at (280, 200)\nblit monster.png at (200, 60)\nblit text \"Dodged: 1\" at (10, 10)","size":[320,240],"title":"Dodge","image":"lessons/windows/85912c5d22e681bc.png"},"expect":"","hidden":true},{"name":"Into the left wall","stdin":"","window":{"frames":100,"events":[{"at":50,"type":"keydown","key":"K_LEFT"},{"at":95,"type":"keyup","key":"K_LEFT"}]},"expectWindow":{"frames":100,"log":"fill (0, 0, 0)\nblit robot.png at (0, 200)\nblit monster.png at (120, 20)\nblit text \"Dodged: 2\" at (10, 10)","size":[320,240],"title":"Dodge","image":"lessons/windows/b281a124407507bb.png"},"expect":"","hidden":true},{"name":"Keys after the game is over","stdin":"","window":{"frames":150,"events":[{"at":135,"type":"keydown","key":"K_RIGHT"},{"at":145,"type":"keyup","key":"K_RIGHT"}]},"expectWindow":{"frames":150,"log":"fill (0, 0, 0)\nblit robot.png at (140, 200)\nblit monster.png at (120, 165)\nblit text \"Dodged: 2\" at (10, 10)\nblit text \"Game over!\" at (115, 100)","size":[320,240],"title":"Dodge","image":"lessons/windows/a1f5e9a80e2b47f6.png"},"expect":"","hidden":true}],"require":[{"pattern":"\\.colliderect\\s*\\(","message":"Check for touching with `colliderect`."}],"forbid":[],"assets":["robot.png","monster.png"]}]}