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()

浙公网安备 33010602011771号