|
1 | | -from random import * |
2 | | -from turtle import * |
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Memory Game – a tile‑matching game using the Turtle graphics library. |
| 4 | +
|
| 5 | +The board consists of 64 tiles (8x8 grid), each hiding one of 32 possible |
| 6 | +icons (numbers 0–31). The player clicks on tiles to reveal them. When two |
| 7 | +consecutive revealed tiles show the same icon, they remain visible; otherwise |
| 8 | +they are hidden again. The game continues until all pairs are found. |
| 9 | +
|
| 10 | +Dependencies: |
| 11 | + - Python standard library: random, turtle |
| 12 | + - freegames (optional) – provides a car image; if not available, remove |
| 13 | + the car-related lines and the game will work without it. |
| 14 | +
|
| 15 | +Author: Adapted from a freegames example. |
| 16 | +Date: 2026-07-12 |
| 17 | +""" |
| 18 | + |
| 19 | +import random |
| 20 | +import turtle |
3 | 21 |
|
4 | 22 | from freegames import path |
5 | 23 |
|
| 24 | +# ---------------------------------------------------------------------------- |
| 25 | +# Game constants and global state |
| 26 | +# ---------------------------------------------------------------------------- |
| 27 | + |
| 28 | +# Load the car image (used as a decorative stamp on the board) |
6 | 29 | car = path("car.gif") |
| 30 | + |
| 31 | +# Create a list of 64 tile values: each number from 0 to 31 appears exactly twice |
7 | 32 | tiles = list(range(32)) * 2 |
| 33 | + |
| 34 | +# State dictionary: 'mark' stores the index of the currently selected tile |
| 35 | +# (the first tile clicked in a pair attempt). None means no tile is selected. |
8 | 36 | state = {"mark": None} |
| 37 | + |
| 38 | +# Boolean list indicating whether each tile is hidden (True = hidden, False = shown) |
9 | 39 | hide = [True] * 64 |
10 | 40 |
|
11 | 41 |
|
12 | | -def square(x, y): |
13 | | - "Draw white square with black outline at (x, y)." |
14 | | - up() |
15 | | - goto(x, y) |
16 | | - down() |
17 | | - color("black", "white") |
18 | | - begin_fill() |
19 | | - for count in range(4): |
20 | | - forward(50) |
21 | | - left(90) |
22 | | - end_fill() |
| 42 | +# ---------------------------------------------------------------------------- |
| 43 | +# Drawing utilities |
| 44 | +# ---------------------------------------------------------------------------- |
| 45 | + |
| 46 | + |
| 47 | +def square(x: float, y: float) -> None: |
| 48 | + """ |
| 49 | + Draw a 50x50 white square with a black outline at the given coordinates. |
| 50 | +
|
| 51 | + The turtle starts at the bottom‑left corner of the square and draws it |
| 52 | + counter‑clockwise using the standard forward/left movements. |
| 53 | +
|
| 54 | + Args: |
| 55 | + x: X‑coordinate of the bottom‑left corner. |
| 56 | + y: Y‑coordinate of the bottom‑left corner. |
| 57 | + """ |
| 58 | + turtle.up() |
| 59 | + turtle.goto(x, y) |
| 60 | + turtle.down() |
| 61 | + turtle.color("black", "white") |
| 62 | + turtle.begin_fill() |
| 63 | + for _ in range(4): |
| 64 | + turtle.forward(50) |
| 65 | + turtle.left(90) |
| 66 | + turtle.end_fill() |
| 67 | + |
| 68 | + |
| 69 | +# ---------------------------------------------------------------------------- |
| 70 | +# Coordinate and index conversion |
| 71 | +# ---------------------------------------------------------------------------- |
| 72 | + |
23 | 73 |
|
| 74 | +def index(x: float, y: float) -> int: |
| 75 | + """ |
| 76 | + Convert screen coordinates to a tile index (0‑63). |
24 | 77 |
|
25 | | -def index(x, y): |
26 | | - "Convert (x, y) coordinates to tiles index." |
| 78 | + The board is arranged in an 8x8 grid, with each cell being 50x50 pixels. |
| 79 | + The origin (0,0) is at the centre of the screen; the board occupies the |
| 80 | + region from (-200, -200) to (200, 200). This function maps a click |
| 81 | + position to the corresponding tile index, row‑major order. |
| 82 | +
|
| 83 | + Args: |
| 84 | + x: X‑coordinate of the click (in turtle screen units). |
| 85 | + y: Y‑coordinate of the click. |
| 86 | +
|
| 87 | + Returns: |
| 88 | + int: Tile index (0 to 63). |
| 89 | + """ |
27 | 90 | return int((x + 200) // 50 + ((y + 200) // 50) * 8) |
28 | 91 |
|
29 | 92 |
|
30 | | -def xy(count): |
31 | | - "Convert tiles count to (x, y) coordinates." |
| 93 | +def xy(count: int) -> tuple[float, float]: |
| 94 | + """ |
| 95 | + Convert a tile index to screen coordinates of its bottom‑left corner. |
| 96 | +
|
| 97 | + This is the inverse of `index()`. The calculation uses integer division |
| 98 | + and modulo to determine the row and column. |
| 99 | +
|
| 100 | + Args: |
| 101 | + count: Tile index (0‑63). |
| 102 | +
|
| 103 | + Returns: |
| 104 | + tuple[float, float]: (x, y) coordinates of the tile's bottom‑left corner. |
| 105 | + """ |
32 | 106 | return (count % 8) * 50 - 200, (count // 8) * 50 - 200 |
33 | 107 |
|
34 | 108 |
|
35 | | -def tap(x, y): |
36 | | - "Update mark and hidden tiles based on tap." |
| 109 | +# ---------------------------------------------------------------------------- |
| 110 | +# Game logic |
| 111 | +# ---------------------------------------------------------------------------- |
| 112 | + |
| 113 | + |
| 114 | +def tap(x: float, y: float) -> None: |
| 115 | + """ |
| 116 | + Handle a mouse click event on the canvas. |
| 117 | +
|
| 118 | + This is the core game logic. It performs the following steps: |
| 119 | + 1. Determine which tile was clicked. |
| 120 | + 2. If no tile is currently marked (state["mark"] is None), |
| 121 | + mark this tile as the first of a pair. |
| 122 | + 3. If a tile is already marked and it is not the same tile, |
| 123 | + compare the values of the marked tile and the newly clicked tile: |
| 124 | + - If they match (same number), reveal both tiles permanently. |
| 125 | + - If they do not match, unmark the tile (keep it hidden). |
| 126 | + 4. If the same tile is clicked twice, it simply remains marked. |
| 127 | +
|
| 128 | + Args: |
| 129 | + x: X‑coordinate of the click. |
| 130 | + y: Y‑coordinate of the click. |
| 131 | + """ |
37 | 132 | spot = index(x, y) |
38 | 133 | mark = state["mark"] |
39 | 134 |
|
| 135 | + # If no mark, or we clicked the same tile again, or the values differ: |
| 136 | + # just set the mark to the new spot (or keep it if it's the same). |
| 137 | + # Note: if the two values are different, we don't hide anything – the |
| 138 | + # tile will be drawn hidden again in the next draw() cycle because |
| 139 | + # hide[spot] remains True. |
40 | 140 | if mark is None or mark == spot or tiles[mark] != tiles[spot]: |
41 | 141 | state["mark"] = spot |
42 | 142 | else: |
| 143 | + # Values match: reveal both tiles permanently. |
43 | 144 | hide[spot] = False |
44 | 145 | hide[mark] = False |
| 146 | + # Clear the mark so the next click starts a new pair. |
45 | 147 | state["mark"] = None |
46 | 148 |
|
47 | 149 |
|
48 | | -def draw(): |
49 | | - "Draw image and tiles." |
50 | | - clear() |
51 | | - goto(0, 0) |
52 | | - shape(car) |
53 | | - stamp() |
54 | | - |
| 150 | +def draw() -> None: |
| 151 | + """ |
| 152 | + Redraw the entire board and schedule the next animation frame. |
| 153 | +
|
| 154 | + This function is called repeatedly via turtle.ontimer() to update the |
| 155 | + display. It: |
| 156 | + - Clears the canvas. |
| 157 | + - Draws the decorative car image at the centre. |
| 158 | + - Draws a white square for every hidden tile. |
| 159 | + - If a tile is marked and currently hidden, it writes the tile's |
| 160 | + value (number) on top of that square. |
| 161 | + - Finally, triggers a redraw after 100 milliseconds. |
| 162 | +
|
| 163 | + The use of turtle.tracer(False) and manual turtle.update() ensures |
| 164 | + smooth animation without flickering. |
| 165 | + """ |
| 166 | + turtle.clear() |
| 167 | + turtle.goto(0, 0) |
| 168 | + turtle.shape(car) |
| 169 | + turtle.stamp() # Place the car image permanently on the canvas |
| 170 | + |
| 171 | + # Draw all tiles that are still hidden. |
55 | 172 | for count in range(64): |
56 | 173 | if hide[count]: |
57 | 174 | x, y = xy(count) |
58 | 175 | square(x, y) |
59 | 176 |
|
| 177 | + # If a tile is marked and hidden, display its value. |
60 | 178 | mark = state["mark"] |
61 | | - |
62 | 179 | if mark is not None and hide[mark]: |
63 | 180 | x, y = xy(mark) |
64 | | - up() |
65 | | - goto(x + 2, y) |
66 | | - color("black") |
67 | | - write(tiles[mark], font=("Arial", 30, "normal")) |
| 181 | + turtle.up() |
| 182 | + turtle.goto(x + 2, y) # Shift slightly for better centering |
| 183 | + turtle.color("black") |
| 184 | + turtle.write(tiles[mark], font=("Arial", 30, "normal")) |
| 185 | + |
| 186 | + turtle.update() # Refresh the screen with all drawn elements |
| 187 | + turtle.ontimer(draw, 100) # Schedule next redraw (10 fps) |
68 | 188 |
|
69 | | - update() |
70 | | - ontimer(draw, 100) |
71 | 189 |
|
| 190 | +# ---------------------------------------------------------------------------- |
| 191 | +# Main setup and execution |
| 192 | +# ---------------------------------------------------------------------------- |
72 | 193 |
|
73 | | -shuffle(tiles) |
74 | | -setup(420, 420, 370, 0) |
75 | | -addshape(car) |
76 | | -hideturtle() |
77 | | -tracer(False) |
78 | | -onscreenclick(tap) |
| 194 | +# Shuffle the tile values to randomise the board layout. |
| 195 | +random.shuffle(tiles) |
| 196 | + |
| 197 | +# Configure the turtle window. |
| 198 | +turtle.setup(420, 420, 370, 0) # Window size, start x, start y |
| 199 | +turtle.addshape(car) # Register the car image as a shape |
| 200 | +turtle.hideturtle() # Hide the turtle cursor (we only need drawings) |
| 201 | +turtle.tracer(False) # Disable automatic updates for performance |
| 202 | +turtle.onscreenclick(tap) # Bind mouse clicks to the tap() function |
| 203 | + |
| 204 | +# Start the game loop. |
79 | 205 | draw() |
80 | | -done() |
| 206 | + |
| 207 | +# Keep the window open (this is the main event loop). |
| 208 | +turtle.done() |
0 commit comments