多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

消消乐小游戏python

消消乐小游戏python 开心消消乐 - Python 重制版无限全屏消除 运行: python xiaoxiaole.py 依赖: pip install pygame 操作: 鼠标点击交换相邻方块 | R 重开 | 空格 全屏消除 | ESC 退出 import os import sys import random import math import pygame # 配置 COLS 8 ROWS 8 CELL 64 PAD 24 TOP 100 BOTTOM 70 BOARD_W COLS * CELL BOARD_H ROWS * CELL W BOARD_W PAD * 2 H TOP BOARD_H BOTTOM FPS 60 # 动画速度 SWAP_FRAMES 6 CLEAR_FRAMES 7 FALL_FRAMES 9 INTERP 0.55 FALL_INTERP 0.65 # 全屏消除相关 SHAKE_DURATION 30 # 动物: (名字, 主色, 深色, 浅色, emoji) ANIMALS [ (熊猫, (245, 245, 245), (200, 200, 200), (255, 255, 255), ), (狐狸, (255, 150, 70), (210, 110, 40), (255, 190, 120), ), (青蛙, (110, 220, 110), (70, 170, 70), (160, 240, 160), ), (小鸡, (255, 220, 90), (210, 170, 50), (255, 240, 150), ), (兔子, (255, 180, 220), (210, 130, 180), (255, 210, 240), ), (猫咪, (180, 150, 255), (130, 100, 220), (210, 190, 255), ), ] TYPE_COUNT len(ANIMALS) # 字体 def find_font(candidates): for p in candidates: if os.path.exists(p): return p return None def get_cn_font(size): path find_font([ C:/Windows/Fonts/msyh.ttc, C:/Windows/Fonts/simhei.ttf, C:/Windows/Fonts/simsun.ttc, /System/Library/Fonts/PingFang.ttc, /usr/share/fonts/truetype/wqy/wqy-microhei.ttc, /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc, ]) if path: return pygame.font.Font(path, size) return pygame.font.Font(None, size) def get_emoji_font(size): path find_font([ C:/Windows/Fonts/seguiemj.ttf, /System/Library/Fonts/Apple Color Emoji.ttc, /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf, ]) if path: try: return pygame.font.Font(path, size) except Exception: return None return None # 方块 class Tile: __slots__ (type, col, row, x, y, sx, sy, scale, target_scale, alpha, target_alpha, selected, falling) def __init__(self, type_idx, col, row, start_yNone): self.type type_idx self.col col self.row row self.x col * CELL self.y row * CELL if start_y is None else start_y self.sx col * CELL self.sy row * CELL self.scale 1.0 self.target_scale 1.0 self.alpha 255.0 self.target_alpha 255.0 self.selected False self.falling False def update(self): interp FALL_INTERP if self.falling else INTERP self.x (self.sx - self.x) * interp self.y (self.sy - self.y) * interp if abs(self.sx - self.x) 0.6: self.x self.sx if abs(self.sy - self.y) 0.6: self.y self.sy self.scale (self.target_scale - self.scale) * interp if abs(self.target_scale - self.scale) 0.02: self.scale self.target_scale self.alpha (self.target_alpha - self.alpha) * interp if abs(self.target_alpha - self.alpha) 3: self.alpha self.target_alpha if self.falling and abs(self.sy - self.y) 1.5: self.falling False # 粒子 class Particle: __slots__ (x, y, vx, vy, life, color, size, gravity) def __init__(self, x, y, color, speed_mult1.0, size_mult1.0): angle random.random() * math.pi * 2 speed (random.random() * 5 1.5) * speed_mult self.x x self.y y self.vx math.cos(angle) * speed self.vy math.sin(angle) * speed - 1.5 self.life 1.0 self.color color self.size (random.random() * 4 2) * size_mult self.gravity 0.22 def update(self): self.x self.vx self.y self.vy self.vy self.gravity self.vx * 0.95 self.life - 0.045 def draw(self, surf): if self.life 0: return alpha int(self.life * 255) r int(self.size * self.life) if r 1: return s pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) pygame.draw.circle(s, (*self.color, alpha), (r, r), r) surf.blit(s, (self.x - r, self.y - r)) # 冲击波 class Shockwave: __slots__ (x, y, radius, max_radius, life, color) def __init__(self, x, y, max_radius, color): self.x x self.y y self.radius 5 self.max_radius max_radius self.life 1.0 self.color color def update(self): self.radius (self.max_radius - self.radius) * 0.18 self.life - 0.035 def draw(self, surf): if self.life 0: return alpha int(self.life * 180) r int(self.radius) if r 2: return s pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) w max(2, int(6 * self.life)) pygame.draw.circle(s, (*self.color, alpha), (r, r), r, w) surf.blit(s, (self.x - r, self.y - r)) # 飘分 class FloatText: __slots__ (text, x, y, vy, life, color, font) def __init__(self, text, x, y, color, font): self.text text self.x x self.y y self.vy -2.0 self.life 1.0 self.color color self.font font def update(self): self.y self.vy self.vy * 0.94 self.life - 0.04 def draw(self, surf): if self.life 0: return alpha int(self.life * 255) s self.font.render(self.text, True, self.color) s.set_alpha(alpha) rect s.get_rect(center(self.x, self.y)) surf.blit(s, rect) # 按钮 class Button: def __init__(self, x, y, w, h, text, color, hover_color, text_color(255, 255, 255)): self.rect pygame.Rect(x, y, w, h) self.text text self.color color self.hover_color hover_color self.text_color text_color self.hovered False self.pulse 0.0 def update(self, mx, my): self.hovered self.rect.collidepoint(mx, my) self.pulse 0.08 def draw(self, surf, font): # 永远脉冲浮动 pulse_offset int(3 * math.sin(self.pulse)) draw_rect self.rect.move(0, pulse_offset) if self.hovered: color self.hover_color glow pygame.Surface((draw_rect.w 20, draw_rect.h 20), pygame.SRCALPHA) glow_color (*self.hover_color, 100) pygame.draw.rect(glow, glow_color, (0, 0, draw_rect.w 20, draw_rect.h 20), border_radius16) surf.blit(glow, (draw_rect.x - 10, draw_rect.y - 10)) else: color self.color # 主体 pygame.draw.rect(surf, color, draw_rect, border_radius12) # 高光 hl_rect pygame.Rect(draw_rect.x 4, draw_rect.y 3, draw_rect.w - 8, draw_rect.h // 3) hl pygame.Surface((hl_rect.w, hl_rect.h), pygame.SRCALPHA) pygame.draw.rect(hl, (255, 255, 255, 80), (0, 0, hl_rect.w, hl_rect.h), border_radius8) surf.blit(hl, hl_rect) # 边框 pygame.draw.rect(surf, (255, 255, 255, 200), draw_rect, 2, border_radius12) # 文字 s font.render(self.text, True, self.text_color) surf.blit(s, (draw_rect.centerx - s.get_width() // 2, draw_rect.centery - s.get_height() // 2 1)) # 游戏 class Game: def __init__(self): pygame.init() self.screen pygame.display.set_mode((W, H)) pygame.display.set_caption(开心消消乐) self.clock pygame.time.Clock() self.font_sm get_cn_font(15) self.font_md get_cn_font(20) self.font_lg get_cn_font(28) self.font_xl get_cn_font(38) self.font_score get_cn_font(22) self.font_btn get_cn_font(18) self.emoji_font get_emoji_font(40) self._emoji_cache {} self._build_emoji_cache() self._static_bg None self._build_static_bg() # 全屏消除按钮永远可用 btn_w, btn_h 180, 44 self.bomb_button Button( (W - btn_w) // 2, H - BOTTOM 10, btn_w, btn_h, 全屏消除, (255, 100, 130), (255, 140, 160), ) self.reset() def _build_emoji_cache(self): if not self.emoji_font: return target int(CELL * 0.62) for i in range(len(ANIMALS)): emoji ANIMALS[i][4] s self.emoji_font.render(emoji, True, (0, 0, 0)) sw, sh s.get_size() if sw target or sh target: ratio target / max(sw, sh) s pygame.transform.smoothscale(s, (int(sw * ratio), int(sh * ratio))) self._emoji_cache[i] s def _build_static_bg(self): surf pygame.Surface((W, H)) for y in range(H): ratio y / H r int(255 - 20 * ratio) g int(240 - 30 * ratio) b int(225 - 20 * ratio) pygame.draw.line(surf, (r, g, b), (0, y), (W, y)) board_rect pygame.Rect(PAD - 6, TOP - 6, BOARD_W 12, BOARD_H 12) pygame.draw.rect(surf, (245, 235, 225), board_rect, border_radius18) pygame.draw.rect(surf, (230, 210, 190), board_rect, 3, border_radius18) for r in range(ROWS): for c in range(COLS): gx c * CELL PAD gy r * CELL TOP color (252, 248, 240) if (c r) % 2 0 else (245, 238, 225) pygame.draw.rect(surf, color, (gx, gy, CELL, CELL)) self._static_bg surf # ---------- 初始化 ---------- def reset(self): self.grid [[None] * COLS for _ in range(ROWS)] self.score 0 self.combo 0 self.selected None self.state idle self.timer 0 self.swap_pair None self.clearing [] self.particles [] self.floats [] self.shockwaves [] self.shake 0 self.bomb_flash 0 self.bomb_count 0 # ⚡ 记录用了多少次纯统计 self.hint_timer 0 for r in range(ROWS): for c in range(COLS): self.grid[r][c] Tile(self._safe_type(c, r), c, r) guard 0 while not self._has_move() and guard 50: self._shuffle() guard 1 def _safe_type(self, c, r): avoid set() if c 2 and self.grid[r][c-1] and self.grid[r][c-2]: if self.grid[r][c-1].type self.grid[r][c-2].type: avoid.add(self.grid[r][c-1].type) if r 2 and self.grid[r-1][c] and self.grid[r-2][c]: if self.grid[r-1][c].type self.grid[r-2][c].type: avoid.add(self.grid[r-1][c].type) choices [i for i in range(TYPE_COUNT) if i not in avoid] return random.choice(choices) # ---------- 匹配检测 ---------- def _find_matches(self): matched set() for r in range(ROWS): c 0 while c COLS: if self.grid[r][c] is None: c 1 continue t self.grid[r][c].type end c 1 while end COLS and self.grid[r][end] and self.grid[r][end].type t: end 1 if end - c 3: for k in range(c, end): matched.add((k, r)) c end for c in range(COLS): r 0 while r ROWS: if self.grid[r][c] is None: r 1 continue t self.grid[r][c].type end r 1 while end ROWS and self.grid[end][c] and self.grid[end][c].type t: end 1 if end - r 3: for k in range(r, end): matched.add((c, k)) r end return matched def _has_move(self): for r in range(ROWS): for c in range(COLS): if c 1 COLS: self.grid[r][c].type, self.grid[r][c1].type \ self.grid[r][c1].type, self.grid[r][c].type has bool(self._find_matches()) self.grid[r][c].type, self.grid[r][c1].type \ self.grid[r][c1].type, self.grid[r][c].type if has: return True if r 1 ROWS: self.grid[r][c].type, self.grid[r1][c].type \ self.grid[r1][c].type, self.grid[r][c].type has bool(self._find_matches()) self.grid[r][c].type, self.grid[r1][c].type \ self.grid[r1][c].type, self.grid[r][c].type if has: return True return False def _shuffle(self): types [] for r in range(ROWS): for c in range(COLS): if self.grid[r][c]: types.append(self.grid[r][c].type) random.shuffle(types) idx 0 for r in range(ROWS): for c in range(COLS): if self.grid[r][c]: self.grid[r][c].type types[idx] idx 1 for r in range(ROWS): for c in range(COLS): t self.grid[r][c] if t: t.x t.sx c * CELL t.y t.sy r * CELL # ---------- 全屏消除无限次 ---------- def use_bomb(self): 全屏消除清除所有方块触发爆炸效果无使用次数限制 if self.state ! idle: return self.bomb_count 1 self.bomb_flash 20 self.shake SHAKE_DURATION all_tiles [] for r in range(ROWS): for c in range(COLS): t self.grid[r][c] if t: all_tiles.append((c, r, t)) if not all_tiles: return total len(all_tiles) * 15 500 self.score total self.floats.append(FloatText( f{total}, W // 2, H // 2 - 60, (255, 230, 100), self.font_xl )) # 冲击波 self.shockwaves.append(Shockwave(W // 2, H // 2, 500, (255, 240, 120))) self.shockwaves.append(Shockwave(W // 2, H // 2, 350, (255, 180, 80))) # 每个方块爆炸粒子 for c, r, t in all_tiles: cx c * CELL CELL // 2 PAD cy r * CELL CELL // 2 TOP color ANIMALS[t.type][1] for _ in range(8): self.particles.append(Particle(cx, cy, color, speed_mult1.5, size_mult1.3)) for _ in range(3): self.particles.append(Particle(cx, cy, (255, 255, 255), speed_mult1.2, size_mult1.0)) self.clearing [(c, r) for c, r, _ in all_tiles] self.state clear self.timer 0 self.combo 0 for c, r, t in all_tiles: t.target_scale 0.0 t.target_alpha 0 # ---------- 交互 ---------- def click(self, mx, my): # 按钮永远可点 if self.bomb_button.rect.collidepoint(mx, my): self.use_bomb() return if self.state ! idle: return bx mx - PAD by my - TOP if bx 0 or by 0 or bx BOARD_W or by BOARD_H: return c bx // CELL r by // CELL if c 0 or c COLS or r 0 or r ROWS: return if self.selected is None: self.selected (c, r) t self.grid[r][c] if t: t.selected True t.target_scale 0.82 else: sc, sr self.selected if (sc, sr) (c, r): t self.grid[sr][sc] if t: t.selected False t.target_scale 1.0 self.selected None return if abs(sc - c) abs(sr - r) 1: t1 self.grid[sr][sc] if t1: t1.selected False t1.target_scale 1.0 self.selected None self.swap_pair ((sc, sr), (c, r)) self.state swap self.timer 0 else: old self.grid[sr][sc] if old: old.selected False old.target_scale 1.0 self.selected (c, r) t self.grid[r][c] if t: t.selected True t.target_scale 0.82 # ---------- 状态更新 ---------- def update(self): mx, my pygame.mouse.get_pos() self.bomb_button.update(mx, my) # 无解提示仅提示不影响按钮使用 if self.state idle and not self._has_move(): self.hint_timer 1 else: self.hint_timer 0 for r in range(ROWS): for c in range(COLS): t self.grid[r][c] if t: t.update() for p in self.particles[:]: p.update() if p.life 0: self.particles.remove(p) for f in self.floats[:]: f.update() if f.life 0: self.floats.remove(f) for sw in self.shockwaves[:]: sw.update() if sw.life 0: self.shockwaves.remove(sw) if self.shake 0: self.shake - 1 if self.bomb_flash 0: self.bomb_flash - 1 if self.state swap: self.timer 1 if self.timer 1: self._start_swap_anim() if self.timer SWAP_FRAMES: self._finish_swap() elif self.state clear: self.timer 1 if self.timer CLEAR_FRAMES: self._finish_clear() elif self.state fall: self.timer 1 if self.timer FALL_FRAMES and self._all_landed(): m self._find_matches() if m: self.combo 1 self._do_clear(m) else: self.state idle self.combo 0 if not self._has_move(): self._shuffle() def _start_swap_anim(self): (c1, r1), (c2, r2) self.swap_pair t1 self.grid[r1][c1] t2 self.grid[r2][c2] if t1: t1.sx c2 * CELL t1.sy r2 * CELL if t2: t2.sx c1 * CELL t2.sy r1 * CELL def _finish_swap(self): (c1, r1), (c2, r2) self.swap_pair self.grid[r1][c1], self.grid[r2][c2] self.grid[r2][c2], self.grid[r1][c1] t1 self.grid[r1][c1] t2 self.grid[r2][c2] if t1: t1.col, t1.row c1, r1 t1.x t1.sx c1 * CELL t1.y t1.sy r1 * CELL if t2: t2.col, t2.row c2, r2 t2.x t2.sx c2 * CELL t2.y t2.sy r2 * CELL m self._find_matches() if m: self.combo 1 self._do_clear(m) else: self.grid[r1][c1], self.grid[r2][c2] self.grid[r2][c2], self.grid[r1][c1] t1 self.grid[r1][c1] t2 self.grid[r2][c2] if t1: t1.col, t1.row c1, r1 t1.x t1.sx c1 * CELL t1.y t1.sy r1 * CELL if t2: t2.col, t2.row c2, r2 t2.x t2.sx c2 * CELL t2.y t2.sy r2 * CELL self.state idle self.swap_pair None def _do_clear(self, matches): self.clearing list(matches) self.state clear self.timer 0 for (c, r) in matches: t self.grid[r][c] if t: t.target_scale 0.0 t.target_alpha 0 base len(matches) * 10 bonus self.combo * 20 total base bonus self.score total self.shake 4 if matches: ax sum(c for c, _ in matches) / len(matches) * CELL CELL // 2 PAD ay sum(r for _, r in matches) / len(matches) * CELL CELL // 2 TOP color (255, 220, 80) if self.combo 3 else (255, 130, 130) self.floats.append(FloatText(f{total}, ax, ay, color, self.font_score)) for (c, r) in matches: t self.grid[r][c] if t: cx c * CELL CELL // 2 PAD cy r * CELL CELL // 2 TOP color ANIMALS[t.type][1] for _ in range(5): self.particles.append(Particle(cx, cy, color)) def _finish_clear(self): for (c, r) in self.clearing: self.grid[r][c] None self.clearing [] for c in range(COLS): stack [] for r in range(ROWS - 1, -1, -1): if self.grid[r][c]: stack.append(self.grid[r][c]) for i, t in enumerate(stack): nr ROWS - 1 - i if t.row ! nr: t.row nr t.col c t.sx c * CELL t.sy nr * CELL t.falling True self.grid[nr][c] t for r in range(ROWS - len(stack)): t Tile(random.randrange(TYPE_COUNT), c, r, start_y-CELL * (ROWS - len(stack) - r) - CELL) t.sy r * CELL t.falling True self.grid[r][c] t self.state fall self.timer 0 def _all_landed(self): for r in range(ROWS): for c in range(COLS): t self.grid[r][c] if t and t.falling: return False return True # ---------- 绘制 ---------- def draw(self): self.screen.blit(self._static_bg, (0, 0)) shake_x random.randint(-3, 3) if self.shake 0 else 0 shake_y random.randint(-3, 3) if self.shake 0 else 0 for r in range(ROWS): for c in range(COLS): t self.grid[r][c] if t and t.alpha 3: self._draw_tile(t, shake_x, shake_y) for p in self.particles: p.draw(self.screen) for sw in self.shockwaves: sw.draw(self.screen) for f in self.floats: f.draw(self.screen) self._draw_header() # 底部按钮永远可用 self.bomb_button.draw(self.screen, self.font_btn) # 无解提示 if self.state idle and not self._has_move(): if (self.hint_timer // 20) % 2 0: hint self.font_sm.render( 没有可消除的组合了点击「全屏消除」清场, True, (255, 80, 120) ) self.screen.blit(hint, (W // 2 - hint.get_width() // 2, TOP - 22)) # 底部快捷键提示 hint self.font_sm.render( 点击相邻方块交换 | 空格 全屏消除无限次 | R 重开 | ESC 退出, True, (160, 140, 120) ) self.screen.blit(hint, (W // 2 - hint.get_width() // 2, H - 18)) # 全屏白闪 if self.bomb_flash 0: alpha int(255 * (self.bomb_flash / 20.0)) flash pygame.Surface((W, H), pygame.SRCALPHA) flash.fill((255, 250, 230, alpha)) self.screen.blit(flash, (0, 0)) def _draw_tile(self, t, sx, sy): size CELL * t.scale if size 2: return cx t.x CELL / 2 PAD sx cy t.y CELL / 2 TOP sy half size / 2 name, color, dark, light, emoji ANIMALS[t.type] rect pygame.Rect(cx - half, cy - half, size, size) radius int(size * 0.22) shadow_surf pygame.Surface((int(size) 8, int(size) 8), pygame.SRCALPHA) pygame.draw.rect(shadow_surf, (0, 0, 0, 60), (4, 4, int(size), int(size)), border_radiusradius) self.screen.blit(shadow_surf, (rect.x - 4, rect.y - 4)) base_color ( int(color[0] * 0.88), int(color[1] * 0.88), int(color[2] * 0.88), ) pygame.draw.rect(self.screen, base_color, rect, border_radiusradius) if size 12: hl_rect pygame.Rect(rect.x size * 0.12, rect.y size * 0.08, size * 0.76, size * 0.32) hl pygame.Surface((int(hl_rect.w), int(hl_rect.h)), pygame.SRCALPHA) pygame.draw.rect(hl, (255, 255, 255, 75), (0, 0, int(hl_rect.w), int(hl_rect.h)), border_radiusmax(2, int(radius * 0.6))) self.screen.blit(hl, hl_rect) border_color (255, 215, 0) if t.selected else dark border_w max(2, int(3 * t.scale)) if t.selected else max(1, int(2 * t.scale)) pygame.draw.rect(self.screen, border_color, rect, border_w, border_radiusradius) if t.alpha 30 and size 20: if self.emoji_font and t.type in self._emoji_cache: s self._emoji_cache[t.type].copy() if t.scale 0.98: sw, sh s.get_size() new_w max(1, int(sw * t.scale)) new_h max(1, int(sh * t.scale)) s pygame.transform.scale(s, (new_w, new_h)) s.set_alpha(int(t.alpha)) self.screen.blit(s, (cx - s.get_width() // 2, cy - s.get_height() // 2)) else: letter name[0] s self.font_lg.render(letter, True, dark) s.set_alpha(int(t.alpha)) self.screen.blit(s, (cx - s.get_width() // 2, cy - s.get_height() // 2)) def _draw_header(self): title self.font_lg.render(开心消消乐, True, (240, 90, 120)) self.screen.blit(title, (W // 2 - title.get_width() // 2, 18)) score_str f分数 {self.score} s self.font_score.render(score_str, True, (90, 70, 50)) self.screen.blit(s, (PAD, 66)) if self.combo 1: c self.font_score.render(f连击 x{self.combo}, True, (255, 100, 100)) self.screen.blit(c, (W - PAD - c.get_width(), 66)) # 右上角显示已用全屏消除次数 if self.bomb_count 0: bc self.font_sm.render(f x{self.bomb_count}, True, (255, 120, 150)) self.screen.blit(bc, (W - PAD - bc.get_width(), 44)) # ---------- 主循环 ---------- def run(self): running True while running: for e in pygame.event.get(): if e.type pygame.QUIT: running False elif e.type pygame.KEYDOWN: if e.key pygame.K_ESCAPE: running False elif e.key pygame.K_r: self.reset() elif e.key pygame.K_SPACE: self.use_bomb() elif e.type pygame.MOUSEBUTTONDOWN and e.button 1: self.click(*e.pos) self.update() self.draw() pygame.display.flip() self.clock.tick(FPS) pygame.quit() sys.exit() if __name__ __main__: print( * 50) print( 开心消消乐) print( * 50) print( 鼠标点击交换相邻方块) print( 空格 / 点击按钮 → 全屏消除无限次) print( R 重开 | ESC 退出) print( * 50) Game().run()
返回列表