python tetr单机版

AI 生成的。

删了一些按键,基本功能保留了。目前支持四十行和两分钟。

代码:

import pygame
import random
import time

COLORS = {
    'I': (0, 255, 255),
    'O': (255, 255, 0),
    'T': (170, 0, 255),
    'S': (0, 255, 0),
    'Z': (255, 0, 0),
    'J': (0, 0, 255),
    'L': (255, 170, 0),
    '.': (30, 30, 30),
}

SHAPES = {
    'I': [['....', 'IIII', '....', '....'],
          ['..I.', '..I.', '..I.', '..I.']],
    'O': [['.OO.', '.OO.', '....', '....']],
    'T': [['....', '.TTT', '..T.', '....'],
          ['..T.', '.TT.', '..T.', '....'],
          ['..T.', '.TTT', '....', '....'],
          ['..T.', '..TT', '..T.', '....']],
    'S': [['....', '..SS', '.SS.', '....'],
          ['.S..', '.SS.', '..S.', '....']],
    'Z': [['....', '.ZZ.', '..ZZ', '....'],
          ['..Z.', '.ZZ.', '.Z..', '....']],
    'J': [['....', '.JJJ', '...J', '....'],
          ['..J.', '..J.', '.JJ.', '....'],
          ['.J..', '.JJJ', '....', '....'],
          ['.JJ.', '.J..', '.J..', '....']],
    'L': [['....', '.LLL', '.L..', '....'],
          ['.LL.', '..L.', '..L.', '....'],
          ['...L', '.LLL', '....', '....'],
          ['.L..', '.L..', '.LL.', '....']]
}
BAG = ['I', 'O', 'T', 'S', 'Z', 'J', 'L']
GRID_W, GRID_H = 10, 22
CELL_SIZE = 30

wall_kick_offsets = [ (0,0), (1,0), (-1,0), (0,1), (0,-1) ]

class Tetrimino:
    def __init__(self, name):
        self.name = name
        self.shapes = SHAPES[name]
        self.rotation = 0
        self.x = 3
        self.y = 0
    def image(self):
        return self.shapes[self.rotation % len(self.shapes)]
    def rotate(self, delta):
        return (self.rotation + delta) % len(self.shapes)

def can_move(grid, tetro, offset_x, offset_y, rotation=None):
    img = tetro.shapes[rotation if rotation is not None else tetro.rotation]
    for y in range(4):
        for x in range(4):
            if img[y][x] == tetro.name:
                wx, wy = tetro.x + x + offset_x, tetro.y + y + offset_y
                if wx < 0 or wx >= GRID_W or wy >= GRID_H:
                    return False
                if wy >= 0 and grid[wy][wx] != '.':
                    return False
    return True

def try_rotate_with_wall_kick(grid, tetro, delta):
    newrot = tetro.rotate(delta)
    for ox, oy in wall_kick_offsets:
        if can_move(grid, tetro, ox, oy, rotation=newrot):
            return newrot, ox, oy
    return None, 0, 0

def merge(grid, tetro):
    img = tetro.image()
    for y in range(4):
        for x in range(4):
            if img[y][x] == tetro.name:
                gx, gy = tetro.x + x, tetro.y + y
                if gy >= 0:
                    grid[gy][gx] = tetro.name

def clear_lines(grid):
    lines = [i for i, row in enumerate(grid) if all(c != '.' for c in row)]
    for i in lines:
        del grid[i]
        grid.insert(0, ['.'] * GRID_W)
    return len(lines)

def get_hard_drop_y(grid, tetro):
    test = Tetrimino(tetro.name)
    test.x, test.y, test.rotation = tetro.x, tetro.y, tetro.rotation
    while can_move(grid, test, 0, 1):
        test.y += 1
    return test.y

def draw_grid(screen, grid, offset_x):
    for y in range(GRID_H):
        for x in range(GRID_W):
            color = COLORS[grid[y][x]]
            pygame.draw.rect(screen, color, (offset_x + x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE))
            pygame.draw.rect(screen, (60, 60, 60), (offset_x + x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE), 1)

def draw_tetromino(screen, tetro, offset_x=0, offset_y=0, ghost=False):
    img = tetro.image()
    for y in range(4):
        for x in range(4):
            if img[y][x] == tetro.name:
                if ghost:
                    base = COLORS[tetro.name]
                    color = (base[0]//2+128, base[1]//2+128, base[2]//2+128)
                else:
                    color = COLORS[tetro.name]
                wx, wy = offset_x + (tetro.x + x) * CELL_SIZE, (tetro.y + y) * CELL_SIZE + offset_y
                pygame.draw.rect(screen, color, (wx, wy, CELL_SIZE, CELL_SIZE))

def draw_mino_4x4_centered(screen, name, box_x, box_y, box_size, cell_size, ghost=False):
    """在目标方框内的中心绘制一个mino"""
    if not name:
        return
    mino_img = SHAPES[name][0]
    min_x = min_y = 4
    max_x = max_y = -1
    # 找到方块真实边界
    for y in range(4):
        for x in range(4):
            if mino_img[y][x] == name:
                min_x = min(min_x, x)
                max_x = max(max_x, x)
                min_y = min(min_y, y)
                max_y = max(max_y, y)
    block_w = max_x - min_x + 1
    block_h = max_y - min_y + 1
    # 计算左上角,使得落于box正中
    cx = box_x + (box_size - block_w * cell_size) // 2
    cy = box_y + (box_size - block_h * cell_size) // 2
    for y in range(4):
        for x in range(4):
            if mino_img[y][x] == name:
                if ghost:
                    base = COLORS[name]
                    color = (base[0]//2+128, base[1]//2+128, base[2]//2+128)
                else:
                    color = COLORS[name]
                wx, wy = cx + (x-min_x)*cell_size, cy + (y-min_y)*cell_size
                pygame.draw.rect(screen, color, (wx, wy, cell_size, cell_size))
                pygame.draw.rect(screen, (60, 60, 60), (wx, wy, cell_size, cell_size), 1)

def draw_hold(screen, hold_name, box_x, box_y, box_size, cell_size):
    txt_font = pygame.font.SysFont("Arial", 24)
    txt = txt_font.render("HOLD", True, (200, 200, 200))
    screen.blit(txt, (box_x + 10, box_y - 32))
    pygame.draw.rect(screen, (80, 80, 80), (box_x, box_y, box_size, box_size), 0)
    pygame.draw.rect(screen, (140, 140, 140), (box_x, box_y, box_size, box_size), 2)
    if hold_name:
        draw_mino_4x4_centered(screen, hold_name, box_x, box_y, box_size, cell_size)

def draw_next(screen, queue, box_x, box_y, box_size, cell_size, count=5):
    txt_font = pygame.font.SysFont("Arial", 24)
    txt = txt_font.render("NEXT", True, (200, 200, 200))
    screen.blit(txt, (box_x + 10, box_y - 32))
    # 大灰框
    H = box_size * count + 15*(count-1)
    pygame.draw.rect(screen, (80, 80, 80), (box_x, box_y, box_size, H), 0)
    pygame.draw.rect(screen, (140, 140, 140), (box_x, box_y, box_size, H), 2)
    # 让所有预览块居中
    for i in range(count):
        if i >= len(queue): break
        by = box_y + i*(box_size+15)
        draw_mino_4x4_centered(screen, queue[i], box_x, by, box_size, cell_size)

def draw_info_bottomleft(screen, mode, lines_cleared, time_used, total_time=120, num_pieces=0, scr_size=(900,660)):
    font = pygame.font.SysFont("Arial", 23)
    MODE_TXT = ["40 LINES", "BLITZ"]
    width, height = scr_size
    px = 18
    py = height - 80
    # PPS (pieces per second)
    pps = num_pieces / time_used if time_used > 0 else 0
    screen.blit(font.render(f"PPS: {pps:.2f}", True, (240, 180, 40)), (px, py-28))
    if mode == 0:
        screen.blit(font.render(f"{MODE_TXT[0]}", True, (200, 210, 230)), (px, py))
        screen.blit(font.render(f"Lines: {lines_cleared}/40", True, (255,255,255)), (px, py+26))
        screen.blit(font.render(f"Time: {time_used:.2f}s", True, (255,255,255)), (px, py+52))
    else:
        remain = max(0, total_time-time_used)
        screen.blit(font.render(f"{MODE_TXT[1]}", True, (200, 210, 230)), (px, py))
        screen.blit(font.render(f"Lines: {lines_cleared}", True, (255,255,255)), (px, py+26))
        screen.blit(font.render(f"Time: {remain:.2f}s", True, (255,255,255)), (px, py+52))

def get_new_bag():
    bag = BAG[:]
    random.shuffle(bag)
    return bag

def home_menu(screen, width, height):
    pygame.display.set_caption("Single Player Tetris")
    tfont = pygame.font.SysFont("Arial", 46, True)
    font = pygame.font.SysFont("Arial", 32)
    title = tfont.render("Single Player Tetris", True, (0,255,255))
    btn1 = pygame.Rect(width//2-160, 220, 320, 70)
    btn2 = pygame.Rect(width//2-160, 320, 320, 70)
    while True:
        screen.fill((20,20,20))
        screen.blit(title, (width//2-title.get_width()//2, 100))
        pygame.draw.rect(screen, (35,160,255), btn1, border_radius=16)
        pygame.draw.rect(screen, (28,120,210), btn2, border_radius=16)
        btn1tx = font.render("40 LINES", True, (255,255,255))
        btn2tx = font.render("BLITZ", True, (255,255,255))
        screen.blit(btn1tx, (btn1.centerx-btn1tx.get_width()//2, btn1.centery-btn1tx.get_height()//2))
        screen.blit(btn2tx, (btn2.centerx-btn2tx.get_width()//2, btn2.centery-btn2tx.get_height()//2))
        pygame.display.flip()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                exit()
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos
                if btn1.collidepoint(mx, my):
                    return 0
                if btn2.collidepoint(mx, my):
                    return 1

def main_game(screen, width, height, mode):
    hold_width = 4 * CELL_SIZE + 20
    play_width = GRID_W * CELL_SIZE
    grid_offset_x = hold_width
    total_time = 120
    offset_left = 0
    grid_cx = hold_width
    next_box_x = grid_cx + play_width + 40
    hold_box_x = 10
    mino_box_size = CELL_SIZE * 4
    hold_box_y = 40
    next_box_y = 40

    grid = [['.'] * GRID_W for _ in range(GRID_H)]
    bag = get_new_bag()
    queue = []
    holding = None
    can_hold = True
    def refill_queue():
        nonlocal bag, queue
        while len(queue) < 6:
            if not bag:
                bag = get_new_bag()
            queue.append(bag.pop())
    def next_tetromino():
        nonlocal bag, queue
        if not queue:
            refill_queue()
        return queue.pop(0)
    curr = Tetrimino(next_tetromino())
    refill_queue()
    running = True
    drop_counter = 0
    soft_drop = False
    lines_cleared = 0
    piece_count = 1  # first one already used
    t0 = time.time()

    das_delay = 150
    arr_interval = 30
    move_left_pressed = False
    move_right_pressed = False
    move_dir = 0
    das_timer = 0
    arr_timer = 0

    lock_delay = 250 # ms
    lock_timer = 0
    lock_ready = False
    hard_dropped = False

    reason = "Game Over"

    while running:
        dt = pygame.time.Clock().tick(60)
        drop_counter += dt
        das_timer += dt
        arr_timer += dt
        if lock_ready:
            lock_timer += dt

        screen.fill((20,20,20))
        draw_hold(screen, holding, hold_box_x, hold_box_y, mino_box_size, CELL_SIZE)
        draw_next(screen, queue, next_box_x, next_box_y, mino_box_size, CELL_SIZE, 5)
        time_now = time.time()
        time_used = time_now - t0
        draw_info_bottomleft(screen, mode, lines_cleared, time_used, total_time, piece_count, (width, height))
        draw_grid(screen, grid, grid_cx)
        ghost = Tetrimino(curr.name)
        ghost.x, ghost.rotation = curr.x, curr.rotation
        ghost.y = get_hard_drop_y(grid, ghost)
        draw_tetromino(screen, ghost, offset_x=grid_cx, ghost=True)
        draw_tetromino(screen, curr, offset_x=grid_cx)
        pygame.display.flip()

        at_bottom = not can_move(grid, curr, 0, 1)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    return None
                if event.key in (pygame.K_LEFT, pygame.K_a):
                    if can_move(grid, curr, -1, 0):
                        curr.x -= 1
                    move_left_pressed = True
                    move_right_pressed = False
                    move_dir = -1
                    das_timer = 0
                    arr_timer = 0
                    if at_bottom:
                        lock_timer = 0
                if event.key in (pygame.K_RIGHT, pygame.K_d):
                    if can_move(grid, curr, 1, 0):
                        curr.x += 1
                    move_right_pressed = True
                    move_left_pressed = False
                    move_dir = 1
                    das_timer = 0
                    arr_timer = 0
                    if at_bottom:
                        lock_timer = 0
                if event.key in (pygame.K_DOWN, pygame.K_s):
                    soft_drop = True
                    if at_bottom:
                        lock_timer = 0
                if event.key in (pygame.K_x, pygame.K_UP):
                    newrot, ox, oy = try_rotate_with_wall_kick(grid, curr, 1)
                    if newrot is not None:
                        curr.rotation = newrot
                        curr.x += ox
                        curr.y += oy
                        if at_bottom:
                            lock_timer = 0
                if event.key == pygame.K_SPACE:
                    curr.y = get_hard_drop_y(grid, curr)
                    merge(grid, curr)
                    nlines = clear_lines(grid)
                    lines_cleared += nlines
                    curr = Tetrimino(next_tetromino())
                    refill_queue()
                    can_hold = True
                    lock_ready = False
                    lock_timer = 0
                    drop_counter = 0
                    hard_dropped = True
                    piece_count += 1
                    if mode == 0 and lines_cleared >= 40:
                        running = False
                        reason = "Congratulations! You cleared 40 lines!"
                    elif mode == 1 and (time.time()-t0) > total_time:
                        running = False
                        reason = f"Time's up! Total lines cleared: {lines_cleared}"
                    elif not can_move(grid, curr, 0, 0):
                        running = False
                        reason = "Game Over"
                if event.key == pygame.K_LSHIFT:
                    if can_hold:
                        if holding:
                            curr, holding = Tetrimino(holding), curr.name
                        else:
                            holding, curr = curr.name, Tetrimino(next_tetromino())
                        can_hold = False
                        refill_queue()
                        if at_bottom:
                            lock_timer = 0
                        piece_count += 1
            elif event.type == pygame.KEYUP:
                if event.key in (pygame.K_LEFT, pygame.K_a):
                    move_left_pressed = False
                    if move_dir == -1: move_dir = 0
                if event.key in (pygame.K_RIGHT, pygame.K_d):
                    move_right_pressed = False
                    if move_dir == 1: move_dir = 0
                if event.key in (pygame.K_DOWN, pygame.K_s):
                    soft_drop = False

        if move_dir != 0:
            is_pressed = (move_left_pressed if move_dir == -1 else move_right_pressed)
            if is_pressed:
                if das_timer > das_delay:
                    if arr_timer > arr_interval:
                        if move_dir == -1 and can_move(grid, curr, -1, 0):
                            curr.x -= 1
                            arr_timer = 0
                            if at_bottom:
                                lock_timer = 0
                        elif move_dir == 1 and can_move(grid, curr, 1, 0):
                            curr.x += 1
                            arr_timer = 0
                            if at_bottom:
                                lock_timer = 0
            else:
                move_dir = 0
        else:
            das_timer = 0
            arr_timer = 0

        drop_speed = 500 if not soft_drop else 50

        if hard_dropped:
            hard_dropped = False
            continue

        if at_bottom:
            if not lock_ready:
                lock_ready = True
                lock_timer = 0
            if lock_timer >= lock_delay:
                merge(grid, curr)
                nlines = clear_lines(grid)
                lines_cleared += nlines
                curr = Tetrimino(next_tetromino())
                refill_queue()
                can_hold = True
                lock_ready = False
                lock_timer = 0
                piece_count += 1
                if mode == 0 and lines_cleared >= 40:
                    running = False
                    reason = "Congratulations! You cleared 40 lines!"
                elif mode == 1 and (time.time()-t0) > total_time:
                    running = False
                    reason = f"Time's up! Total lines cleared: {lines_cleared}"
                elif not can_move(grid, curr, 0, 0):
                    running = False
                    reason = "Game Over"
        else:
            lock_ready = False
            lock_timer = 0
            if drop_counter > drop_speed:
                if can_move(grid, curr, 0, 1):
                    curr.y += 1
                drop_counter = 0

    screen.fill((20,20,20))
    font = pygame.font.SysFont("Arial",36)
    txt = font.render(reason, True, (255,80,80))
    screen.blit(txt, (width//2-txt.get_width()//2, height//2-txt.get_height()//2))
    if mode == 0 and lines_cleared>=40:
        t1 = time.time()
        time_used = t1 - t0
        res = f"Your time: {time_used:.2f} seconds"
        t2 = font.render(res, True, (180,250,250))
        screen.blit(t2, (width//2-t2.get_width()//2, height//2+txt.get_height()))
    elif mode == 1:
        t2 = font.render(f"Total lines: {lines_cleared}", True, (180,250,250))
        screen.blit(t2, (width//2-t2.get_width()//2, height//2+txt.get_height()))
    pygame.display.flip()
    pygame.time.wait(2000)
    return None

def main():
    pygame.init()
    hold_width = 4 * CELL_SIZE + 20
    play_width = GRID_W * CELL_SIZE
    width = hold_width * 2 + play_width + 140
    height = CELL_SIZE*GRID_H
    screen = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Single Player Tetris")
    while True:
        mode = home_menu(screen, width, height)
        res = main_game(screen, width, height, mode)

if __name__ == '__main__':
    main()
posted @ 2026-04-17 18:31  wangtairan114  阅读(10)  评论(0)    收藏  举报