初学python,实现了一个控制台上的俄罗斯方块。(没有使用pygame)。
【环境:python3,使用python2可能在显示时有编码问题,不过不影响功能。】
先说一下思路:
采用局部屏幕刷新,精确到每一个方块,除初始化时刷新了两块显示方块的区域。(没有使用sys("cls"),不会闪屏)。
使用 ctypes 模块调用SetConsoleCursorPosition( )设置光标位置,负责打印方块和打印空白。
使用 msvcrt 模块调用kbhit( )函数,负责响应键盘消息。 【这两个函数的使用百度都有例子,不多赘述。】
显示方块的盒子里的方块信息 使用一个二维列表储存,以检测方块的移动是否合法。二维数组的下标也是方块在屏幕上的相对坐标。相对坐标根据一个特定的初始点(其相对坐标为(0,0))的绝对坐标可以转换成相对坐标。
每一个特定形状的方块都是若干的坐标对象的列表,根据这个列表,可以向二维列表或屏幕写入方块字符,写入屏幕是为了显示,写入二维列表是为了检测合法性。
方块旋转使用了 坐标旋转公式,百度百科有。
方块类型只定义了最基本的7个块,生成方块时会随机选择一个,并旋转随机数次,从而保证随机性。
代码有点长,大概400行,但功能还算全。
也就这么多了,下面是代码和游戏截图。。。没有使用类实现,需要类可以把全局变量前加上self.,再用class包起来就行。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# ersfk.py
#
# Copyright 2018 Tommy
import ctypes
import time
import copy
import random
import msvcrt
#这个是坐标结构体,调用C api需要用到
class COORD(ctypes.Structure): #define a structure in python to use C api
_fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]
def __init__(self,x,y):
self.X = x
self.Y = y
global STD_OUTPUT_HANDLE
STD_OUTPUT_HANDLE= -11
global std_out_handle
std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
COLUMN_T = 16
ROW_T = 30
INIT_POS = COORD(12,1) #absolute pos
DEGREE_OF_DIFFICULTY = 25 #the smaller the harder
global DIAMOND
DIAMOND = {"BLOCKS":"█" , "BLANK":" " , "BOUNDARY":"▓" , "PREVIEW":"○" ,"PERISH":"☺"}
global BOX
BOX = [[DIAMOND["BLANK"]for _ in range(COLUMN_T)]for _ in range(ROW_T)]
#定义方块的类型
global BASE_BLOCK
BASE_BLOCK = {
"Z" :[COORD(0,0),COORD(-1,0),COORD(0,1),COORD(1,1)],
"rZ":[COORD(0,0),COORD(1,0),COORD(0,1),COORD(-1,1)],
"O" :[COORD(0,0),COORD(1,0),COORD(0,1),COORD(1,1)],
"L" :[COORD(0,0),COORD(0,1),COORD(0,2),COORD(1,2)],
"rL":[COORD(0,0),COORD(0,1),COORD(0,2),COORD(-1,2)],
"I" :[COORD(0,0),COORD(0,1),COORD(0,2),COORD(0,3)],
"T" :[COORD(0,0),COORD(-1,0),COORD(1,0),COORD(0,1)]
}
#在坐标coord处填充方块的函数
def fill_pos(coord,char):
tmp_coord = COORD(coord.X * 2 + INIT_POS.X,coord.Y + INIT_POS.Y) #trans relative pos to absolute pos
ctypes.windll.kernel32.SetConsoleCursorPosition(std_out_handle,tmp_coord)
print(char)
def move_down(BLOCK):
if ismove(BLOCK,"down"):
perish_block(BLOCK)
generate_block(BLOCK,"down")
else:
perish_line()
check_lose()
if GAME_FLAG == "ON GAME":
creat_block()
def move_left(BLOCK):
if ismove(BLOCK,"left"):
perish_block(BLOCK)
generate_block(BLOCK,"left")
preview(BLOCK) #显示预览方块
generate_block(BLOCK) #再次调用generate的原因:当预览方块与原方块有重合时,将原方块显示到预览方块上方
def move_right(BLOCK):
if ismove(BLOCK,"right"):
perish_block(BLOCK)
generate_block(BLOCK,"right")
preview(BLOCK)
generate_block(BLOCK)
def isin(BLOCK,one_block):
in_flag = False
for each_block_coord in BLOCK:
if each_block_coord.X == one_block.X and \
each_block_coord.Y == one_block.Y:
in_flag = True
break
return in_flag
def ismove(BLOCK,DIRECTION = "original",BE_SPINED_BLOCK = None):
move_flag = True
if DIRECTION == "down":
for each_block_coord in BLOCK:
tmp_block = COORD(each_block_coord.X,each_block_coord.Y + 1)
if not isin(BLOCK,tmp_block):
if BOX[each_block_coord.Y + 1][each_block_coord.X] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y + 1][each_block_coord.X] == DIAMOND["BOUNDARY"]:
move_flag = False
break
elif DIRECTION == "left":
for each_block_coord in BLOCK:
tmp_block = COORD(each_block_coord.X - 1,each_block_coord.Y)
if not isin(BLOCK,tmp_block):
if BOX[each_block_coord.Y][each_block_coord.X - 1] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y][each_block_coord.X - 1] == DIAMOND["BOUNDARY"]:
move_flag = False
break
elif DIRECTION == "right":
for each_block_coord in BLOCK:
tmp_block = COORD(each_block_coord.X + 1,each_block_coord.Y)
if not isin(BLOCK,tmp_block):
if BOX[each_block_coord.Y][each_block_coord.X + 1] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y][each_block_coord.X + 1] == DIAMOND["BOUNDARY"]:
move_flag = False
break
elif DIRECTION == "spin":
for each_block_coord in BLOCK:
tmp_block = COORD(each_block_coord.X,each_block_coord.Y)
if not isin(BE_SPINED_BLOCK,tmp_block):
if each_block_coord.X >= len(BOX[each_block_coord.Y]) or \
each_block_coord.X < 0 or \
BOX[each_block_coord.Y][each_block_coord.X] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y][each_block_coord.X] == DIAMOND["BOUNDARY"]:
move_flag = False
break
else:
for each_block_coord in BLOCK:
if each_block_coord.X >= len(BOX[each_block_coord.Y]) or \
each_block_coord.X < 0 or \
BOX[each_block_coord.Y][each_block_coord.X] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y][each_block_coord.X] == DIAMOND["BOUNDARY"]:
move_flag = False
break
return move_flag
#更新方块的函数,即方块移动后将方块显示在新的位置
def generate_block(BLOCK,DIRECTION = "original",DIAMOND = DIAMOND["BLOCKS"]):
for each_block_coord in BLOCK:
if DIRECTION == "down":
each_block_coord.Y += 1
elif DIRECTION == "left":
each_block_coord.X -= 1
elif DIRECTION == "right":
each_block_coord.X += 1
BOX[each_block_coord.Y][each_block_coord.X] = DIAMOND
if each_block_coord.Y >= 3:
fill_pos(each_block_coord,DIAMOND)
def perish_block(BLOCK):
for each_block_coord in BLOCK:
BOX[each_block_coord.Y][each_block_coord.X] = DIAMOND["BLANK"]
if each_block_coord.Y >= 3:
fill_pos(each_block_coord,DIAMOND["BLANK"])
def preview(BLOCK):
global PREVIEW_BLOCK
perish_block(PREVIEW_BLOCK)
move_flag = True
for i in range(1,len(BOX)):
for each_block_coord in BLOCK:
tmp_block = COORD(each_block_coord.X,each_block_coord.Y + i)
if not isin(BLOCK,tmp_block):
if BOX[each_block_coord.Y + i][each_block_coord.X] == DIAMOND["BLOCKS"] or \
BOX[each_block_coord.Y + i][each_block_coord.X] == DIAMOND["BOUNDARY"]:
j = i - 1
move_flag = False
PREVIEW_BLOCK = copy.deepcopy(BLOCK)
for each_block_coord in PREVIEW_BLOCK:
each_block_coord.Y += j
BOX[each_block_coord.Y][each_block_coord.X] = DIAMOND["PREVIEW"]
if each_block_coord.Y > 3:
fill_pos(each_block_coord,DIAMOND["PREVIEW"])
break
if not move_flag:
break
def perish_line():
global GAME_SCORE
offset = 0
for y in range(1,len(BOX)-1-2): # 2 is the top bound of the box on screen
if DIAMOND["BLANK"] not in BOX[len(BOX)-y+offset-1]:
for x in range(1,len( BOX[len(BOX)-y+offset-1])-1 ):
generate_block([COORD(x,len(BOX)-(y-offset)-1)],DIRECTION = "original",DIAMOND = DIAMOND["PERISH"])
time.sleep(0.006)
perish_block([COORD(x,len(BOX)-(y-offset)-1)])
time.sleep(0.003)
GAME_SCORE += 10
fill_pos(COORD(25,18),"GAME SCORE: " + str(GAME_SCORE))
offset += 1
for _y in range(1+y-offset,len(BOX)-2): # 2 is the top bound of the box on screen
if DIAMOND["BLOCKS"] not in BOX[_y]:
continue
else:
line = []
for _x in range(1,len( BOX[len(BOX)-_y])-1 ):
if BOX[_y][_x] == DIAMOND["BLOCKS"]:
line.append(COORD(_x,_y))
if ismove(line,"down"):
perish_block(line)
generate_block(line,"down")
time.sleep(0.015)
def spin_pos(BLOCK,offset = (0,0)):
max_y = max(BLOCK,key = lambda b:b.Y).Y
min_x = min(BLOCK,key = lambda b:b.X).X
for each_block_coord in BLOCK:
x_tmp = each_block_coord.X
each_block_coord.X = -(each_block_coord.Y - BLOCK[0].Y) + BLOCK[0].X
each_block_coord.Y = (x_tmp - BLOCK[0].X) + BLOCK[0].Y
min_x_n = min(BLOCK,key = lambda b:b.X).X
max_y_n = max(BLOCK,key = lambda b:b.Y).Y
for each_block_coord in BLOCK:
each_block_coord.X += ((min_x - min_x_n) + offset[0])
each_block_coord.Y += ((max_y - max_y_n) + offset[1])
def isspin(BLOCK):
offset = 0
spin_flag = True
SPIN_BLOCK = copy.deepcopy(BLOCK)
spin_pos(SPIN_BLOCK)
if not ismove(SPIN_BLOCK,DIRECTION = "spin",BE_SPINED_BLOCK = BLOCK):
for each_block_coord in BLOCK:
BOX[each_block_coord.Y][each_block_coord.X] = DIAMOND["BLANK"]
while not ismove(SPIN_BLOCK):
if ismove(SPIN_BLOCK,DIRECTION = "left"):
offset -= 1
for each_block_coord in SPIN_BLOCK:
each_block_coord.X -= 1
else:
spin_flag = False
break
for each_block_coord in BLOCK:
BOX[each_block_coord.Y][each_block_coord.X] = DIAMOND["BLOCKS"]
return (spin_flag,offset)
def spin(BLOCK):
if isspin(BLOCK)[0]:
offset = isspin(BLOCK)[1]
perish_block(BLOCK)
spin_pos(BLOCK,(offset,0))
preview(BLOCK)
generate_block(BLOCK)
else:
pass
def check_lose():
global GAME_FLAG
if DIAMOND["BLOCKS"] in BOX[2] or DIAMOND["PREVIEW"] in BOX[2]: # 2 is the top bound of the box on screen
generate_block(BLOCK) #remedy the last block, cover DIAMOND["PREVIEW"] by DIAMOND["BLOCKS"]
GAME_FLAG = "FAIL "
fill_pos(COORD(25,16),"GAME STATUS: " + GAME_FLAG)
def drop(BLOCK):
perish_block(BLOCK)
generate_block(PREVIEW_BLOCK)
perish_line()
check_lose()
if GAME_FLAG == "ON GAME":
creat_block()
def creat_next_block():
global NEXT_BLOCK
for each_block_coord in NEXT_BLOCK:
fill_pos(each_block_coord,DIAMOND["BLANK"])
NEXT_BLOCK = copy.deepcopy(BASE_BLOCK[list(BASE_BLOCK.keys())[random.randint(0,len(BASE_BLOCK)-1)]])
for k in range(0,random.randint(0,4)):
spin_pos(NEXT_BLOCK)
min_x = min(NEXT_BLOCK,key = lambda b:b.X).X
max_y = max(NEXT_BLOCK,key = lambda b:b.Y).Y
for each_block_coord in NEXT_BLOCK:
each_block_coord.X += (COLUMN_T + 12 - min_x)
each_block_coord.Y += (10 - max_y)
fill_pos(each_block_coord,DIAMOND["BLOCKS"])
def creat_block():
global BLOCK
global NEXT_BLOCK
global PREVIEW_BLOCK
BLOCK = copy.deepcopy(NEXT_BLOCK)
creat_next_block()
PREVIEW_BLOCK = []
max_y = max(BLOCK,key = lambda b:b.Y).Y
min_x = min(BLOCK,key = lambda b:b.X).X
for each_block_coord in BLOCK:
each_block_coord.X += (int(COLUMN_T/2)-1 - min_x)
each_block_coord.Y += (3 - max_y)
generate_block(BLOCK)
preview(BLOCK)
def init():
global GAME_SCORE
GAME_SCORE = 0
global GAME_FLAG
GAME_FLAG = "ON GAME"
global times
times = 0
for r in range(0,ROW_T):
if r < 2:
BOX[r][0] = DIAMOND["BOUNDARY"]
BOX[r][COLUMN_T - 1] = DIAMOND["BOUNDARY"]
for c in range(1,COLUMN_T - 1):
fill_pos(COORD(c,r),DIAMOND["BLANK"])
BOX[r][c] = DIAMOND["BLANK"]
elif r == 2: # 2 is the top bound of the box on screen
for c in range(COLUMN_T):
fill_pos(COORD(c,r),DIAMOND["BOUNDARY"])
for c in range(1,COLUMN_T - 1):
BOX[r][c] = DIAMOND["BLANK"]
elif r > 2 and r < ROW_T - 1:
BOX[r][0] = DIAMOND["BOUNDARY"]
fill_pos(COORD(0,r),DIAMOND["BOUNDARY"])
BOX[r][COLUMN_T - 1] = DIAMOND["BOUNDARY"]
fill_pos(COORD(COLUMN_T - 1,r),DIAMOND["BOUNDARY"])
for c in range(1,COLUMN_T - 1):
fill_pos(COORD(c,r),DIAMOND["BLANK"])
BOX[r][c] = DIAMOND["BLANK"]
elif r == ROW_T - 1:
for c in range(COLUMN_T):
BOX[r][c] = DIAMOND["BOUNDARY"]
fill_pos(COORD(c,r),DIAMOND["BOUNDARY"])
fill_pos(COORD(25,3),"NEXT BLOCK:")
for r in range(10):
for c in range(8):
fill_pos(COORD(26+c,4+r),DIAMOND["BLANK"])
fill_pos(COORD(25,16),"GAME STATUS: " + GAME_FLAG)
fill_pos(COORD(25,18),DIAMOND["BLANK"]*20)
fill_pos(COORD(25,18),"GAME SCORE: " + str(GAME_SCORE))
fill_pos(COORD(25,22),"← → ↓ TO MOVE ↑ TO SPIN")
fill_pos(COORD(25,24),"SPACE TO PAUSE")
fill_pos(COORD(25,26),"ENTER TO RESTART")
fill_pos(COORD(25,28),"ESC TO EXIT")
global NEXT_BLOCK
NEXT_BLOCK = copy.deepcopy(BASE_BLOCK[list(BASE_BLOCK.keys())[random.randint(0,len(BASE_BLOCK)-1)]])
for k in range(0,random.randint(0,3)):
spin_pos(NEXT_BLOCK)
min_x = min(NEXT_BLOCK,key = lambda b:b.X).X
max_y = max(NEXT_BLOCK,key = lambda b:b.Y).Y
for each_block_coord in NEXT_BLOCK:
each_block_coord.X += (COLUMN_T + 12 - min_x)
each_block_coord.Y += (10 - max_y)
def kbfunc(): # Response to the keyboard message
x = msvcrt.kbhit()
if x:
ret = ord(msvcrt.getch())
else:
ret = None
return ret
def main():
global times
global GAME_FLAG
init()
creat_block()
while True:
if GAME_FLAG == "ON GAME":
times+=1
time.sleep(.01)
if times == DEGREE_OF_DIFFICULTY:
move_down(BLOCK)
times = 0
r = kbfunc()
if r == 75:
move_left(BLOCK)
elif r == 77:
move_right(BLOCK)
elif r == 80:
drop(BLOCK)
elif r == 72:
spin(BLOCK)
elif r == 27:
exit()
elif r == 32:
GAME_FLAG = "PAUSE "
fill_pos(COORD(25,16),"GAME STATUS: " + GAME_FLAG)
elif r == 13:
init()
creat_block()
elif GAME_FLAG == "PAUSE ":
r = kbfunc()
if r == 32:
GAME_FLAG = "ON GAME"
fill_pos(COORD(25,16),"GAME STATUS: " + GAME_FLAG)
elif r == 13:
init()
creat_block()
elif r == 27:
exit()
else:
r = kbfunc()
if r == 13:
init()
creat_block()
elif r == 27:
exit()
if __name__ == "__main__":
main()