把 4×4 共 16 张卡片随机打乱,点击翻开两张,图案相同就消掉,不同就翻回去。考验记忆力的同时,练会"状态机":翻牌前、翻一张、翻两张、配对成功、配对失败。

1. 卡片数据与洗牌

16 张卡片 = 8 对图案。用数字 0~7 各出现两次表示图案,洗牌用 random.shuffle。每张卡片记录:图案、是否已翻开、是否已配对:

import random, pygame

COLS = ROWS = 4
N = COLS * ROWS
pairs = list(range(N // 2)) * 2      # 0~7 各两次
random.shuffle(pairs)                 # 洗牌
state = ['closed'] * N                # closed / open / matched

2. 点击与翻开

鼠标点击换算成卡片下标,只有"关着的卡片"能翻开。翻开第一张记录位置,翻开第二张做判断:

flipped = -1   # 当前翻开的第一张下标

def on_click(card):
    global flipped
    if state[card] != 'closed':
        return
    if flipped == -1:
        state[card] = 'open'
        flipped = card
    else:
        state[card] = 'open'
        if pairs[flipped] == pairs[card]:   # 配对成功
            state[flipped] = state[card] = 'matched'
        else:                                # 失败:短暂展示后翻回
            state[flipped] = state[card] = 'closed'
        flipped = -1

3. 绘制卡片

用 pygame.Rect 画卡片:关着的画背面(纯色+问号),翻开的画图案(不同颜色方块)。配对成功的画半透明,表示已消除:

CELL = 100
GAP = 12
W = H = COLS * CELL + (COLS + 1) * GAP
screen = pygame.display.set_mode((W, H))

def draw():
    screen.fill((30, 30, 45))
    for i in range(N):
        x = GAP + (i % COLS) * (CELL + GAP)
        y = GAP + (i // COLS) * (CELL + GAP)
        rect = (x, y, CELL, CELL)
        if state[i] == 'closed':
            pygame.draw.rect(screen, (70, 90, 140), rect, border_radius=10)
        elif state[i] == 'open':
            pygame.draw.rect(screen, (220, 180, 90), rect, border_radius=10)
            color = [(220,60,60),(60,160,220),(90,200,90),(220,150,50),
                     (180,90,220),(60,200,200),(230,90,120),(160,160,60)][pairs[i]]
            pygame.draw.circle(screen, color, (x+CELL//2, y+CELL//2), 22)
        else:
            pygame.draw.rect(screen, (60, 200, 120), rect, border_radius=10)
    pygame.display.flip()

4. 完整代码

import random, pygame

COLS = ROWS = 4
N = COLS * ROWS
CELL = 100
GAP = 12
W = H = COLS * CELL + (COLS + 1) * GAP

pygame.init()
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption('记忆翻牌')
clock = pygame.time.Clock()

pairs = list(range(N // 2)) * 2
random.shuffle(pairs)
state = ['closed'] * N
flipped = -1
moves = 0
won = False

def draw():
    screen.fill((30, 30, 45))
    for i in range(N):
        x = GAP + (i % COLS) * (CELL + GAP)
        y = GAP + (i // COLS) * (CELL + GAP)
        rect = (x, y, CELL, CELL)
        if state[i] == 'closed':
            pygame.draw.rect(screen, (70, 90, 140), rect, border_radius=10)
        elif state[i] == 'open':
            pygame.draw.rect(screen, (220, 180, 90), rect, border_radius=10)
            color = [(220,60,60),(60,160,220),(90,200,90),(220,150,50),
                     (180,90,220),(60,200,200),(230,90,120),(160,160,60)][pairs[i]]
            pygame.draw.circle(screen, color, (x+CELL//2, y+CELL//2), 22)
        else:
            pygame.draw.rect(screen, (60, 200, 120), rect, border_radius=10)
    pygame.display.set_caption('记忆翻牌 · 步数 %d' % moves)
    pygame.display.flip()

while not won:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            pygame.quit(); raise SystemExit
        if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
            mx, my = pygame.mouse.get_pos()
            c = (my - GAP) // (CELL + GAP)
            r = (mx - GAP) // (CELL + GAP)
            if 0 <= c < COLS and 0 <= r < ROWS:
                card = c * COLS + r
                if state[card] == 'closed':
                    moves += 1
                    if flipped == -1:
                        state[card] = 'open'
                        flipped = card
                    else:
                        state[card] = 'open'
                        if pairs[flipped] == pairs[card]:
                            state[flipped] = state[card] = 'matched'
                        else:
                            state[flipped] = state[card] = 'closed'
                        flipped = -1
    won = all(s == 'matched' for s in state)
    draw()
    clock.tick(30)
print('通关!用了 %d 步' % moves)

5. 运行与常见问题

保存为 memory.py,python3 memory.py 运行。常见问题:①点同一张卡算一次——翻第一张后,判断里要先排除当前卡片本身;②图案太少/太多——pairs = list(range(N//2)) * 2 决定了图案数,改成 6×6 就是 18 对;③想加难度——限定步数或加计时。

💡 改进方向:①翻牌动画(缩放/翻转);②计时与最佳纪录;③图案换成 emoji 或图片;④配对成功音效;⑤多难度棋盘。