[pygame]Python不到300行代码实现俄罗斯方块本文代码基于python3.6和pygame1.9.4.俄罗斯方块是最经典的童年游戏之一。刚开始接触pygame的时候,想写个俄罗斯方块。但是涉及到旋转、对接、消除等操作时,就显得困难重重了。等写完了,发现总共只有300行代码,没什么难的。我们先来看看游戏截图。有点难看。嗯,美术功底不高,但是主要功能已经实现了,可以玩了。下面我们来看一下实现过程。外观俄罗斯方块整个界面分为两部分,左边是游戏区,右边是显示区,显示得分,速度,下一方块样式等,我就不放了这里截图,看上图就知道了。和贪吃蛇一样,游戏区域由小方块组成。为了直观看,我特地画了网格线。importsysimportpygamefrompygame.localsimport*SIZE=30#每个小方块的大小BLOCK_HEIGHT=20#游戏区域高度BLOCK_WIDTH=10#游戏区域宽度BORDER_WIDTH=4#游戏区域边框宽度BORDER_COLOR=(40,40,200)#游戏区域边框颜色SCREEN_WIDTH=SIZE*(BLOCK_WIDTH+5)#游戏屏幕宽度SCREEN_HEIGHT=SIZE*BLOCK_HEIGHT#游戏屏幕高度BG_COLOR=(40,40,60)#背景颜色BLACK=(0,0,0)defprint_text(screen,字体,x,y,文本,fcolor=(255,255,255)):imgText=font.render(text,True,fcolor)screen.blit(imgText,(x,y))defmain():pygame.init()screen=pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT))pygame.display.set_caption('Tetris')font1=pygame.font.SysFont('SimHei',24)#bold24font_pos_x=BLOCK_WIDTH*SIZE+BORDER_WIDTH+10#右侧信息显示区字体位置的X坐标font1_height=int(font1.size('score')[1])score=0#scorewhileTrue:foreventinpygame.event.get():ifevent.type==QUIT:sys.exit()#填充背景色screen.fill(BG_COLOR)#绘制游戏区域分隔线pygame.draw.line(screen,BORDER_COLOR,(SIZE*BLOCK_WIDTH+BORDER_WIDTH//2,0),(SIZE*BLOCK_WIDTH+BORDER_WIDTH//2,SCREEN_HEIGHT),BORDER_WIDTH)#绘制垂直网格线forxinrange(BLOCK_WIDTH):pygame.draw.line(screen,BLACK,(x*SIZE,0),(x*SIZE,SCREEN_HEIGHT),1)#在范围内为y绘制网格线(BLOCK_HEIGHT):pygame.draw.line(screen,BLACK,(0,y*SIZE),(BLOCK_WIDTH*SIZE,y*SIZE),1)print_text(screen,font1,font_pos_x,10,f'score:')print_text(screen,font1,font_pos_x,10+font1_height+6,f'{分数}')print_text(screen,font1,font_pos_x,20+(font1_height+6)*2,f'speed:')print_text(screen,font1,font_pos_x,20+(font1_height+6)*3,f'{score//10000}')print_text(screen,font1,font_pos_x,30+(font1_height+6)*4,f'next:')pygame.display.flip()ifname=='__main__':main()接下来就是定义块。积木有7种形状:I型、O型、T型、S型、Z型、L型、J型。这里我做了很多改动,因为积木的最大长度是长的。,是4个格子,所以我统一用4×4格子来定义这个也是可以的,但是后来发现不方便。为了直观起见,直接用一个二维数组来定义正方形,其中.表示空,0表示实心。(用.表示空是为了直观查看,如果用空格,会看不清楚。)比如第I行定义为['.0..','.0..','.0with4×4squares..','.0..']and['....','....','0000','..']最困难的是实现旋转功能,比如IType,有水平和垂直两种形式。所谓旋转,表面上看就是将立方体顺时针旋转90°,但实际做的时候,并不需要真正达到这种“旋转”的效果。最终实现的时候,这些图形都是我们绘制在界面上的,每次刷新时,界面上的所有内容都会清空,重新绘制,所以旋转就是在绘制当前块的时候,不画之前的形状,而是画形状之后的旋转。例如,I型定义为4×4的形状,但实际上只需要1×4或4×1,其余空间为空。不像T型,T型不是矩形,如果用矩形定义,必须有2个空位。那么,TypeI真的有必要定义为4×4吗?答案是肯定的。想一想,如果是4×1的横条,旋转后变成1×4的竖条,这个位置是怎么确定的呢?好像有点难。但是如果是4×4的正方形,我们只需要固定起点坐标(左上角)不变,直接将竖条的4×4区域替换为竖条的4×4区域即可单杠,旋转会实现吗?而且位置很容易计算。还有一点就是在某些情况下是不能旋转的。比如工字竖条靠近左右边框就不能旋转。我对此有印象,这是肯定的。但是对于其他的形状,我不是很确定。在百度上搜索了一下,找了个网页版的俄罗斯方块玩了一下,发现不可以。例如:靠近右边框时不能旋转。每一个形状都去评判就太烦人了。从block的定义开始,可以很简单的实现。比如竖条线的定义是:['.0..','.0..','.0..','.0..']竖条可以对齐,所以当它在最左边时,X轴坐标为-1,因为定义中左边竖排为空。我们只要确定只有当方块定义的形状(包括空白部分)完全在游戏区域内时,才能进行旋转。前面说过了,都定义成4×4不好,原因就在这里。对于其他的形状比如T字,就不能做这个判断了。因此,对于T字等形状,我们可以将其定义为3×3的格式:['.0.','000','...']还有一种情况是不能旋转的,即旋转的位置已被另一个块占用。此外,还要判断行踪和左右移动。由于这些是一致的,所以可以用同样的方法来判断。首先定义一个game_area变量,用来存放整个游戏区域的当前状态:game_area=[['.']*BLOCK_WIDTHfor_inrange(BLOCK_HEIGHT)]初始状态全为空,所以全部使用。只需初始化。另外,还需要一些变量来定义当前下落块的状态cur_block=None#当前下落块cur_pos_x,cur_pos_y=0,0#当前下落块的坐标块定义为一个二维数组,和有空行和空列,如果我们遍历这个二维数组,判断它所在的区域是否已经被当前游戏区域的其他方块占据,这是可以实现的。再考虑另一种情况,一个竖条,左边一行是空的,这个空行可以移出游戏区域,这个怎么判断呢?每次向左移动,是否判断左行全部为空?这太麻烦了。而且正方形是固定的,所以我们可以提前定义这些。最终块定义如下:fromcollectionsimportnamedtuplePoint=namedtuple('Point','XY')Block=namedtuple('Block','templatestart_posend_posnamenext')S形块S_BLOCK=[Block(['.00','00.','...'],Point(0,0),Point(2,1),'S',1),Block(['0..','00.','.0.'],Point(0,0),Point(1,2),'S',0)]方块需要包含两个方法,旋转BLOCKS时获取随机方块和获取旋转方块={'O':O_BLOCK,'I':I_BLOCK,'Z':Z_BLOCK,'T':T_BLOCK,'L':L_BLOCK,'S':S_BLOCK,'J':J_BLOCK}defget_block():block_name=random.choice('OIZTLSJ')b=BLOCKS[block_name]idx=random.randint(0,len(b)-1)returnb[idx]得到旋转块defget_next_block(block):b=BLOCKS[block.name]returnb[block.next]判断是否可以旋转、下落、移动的方法也很容易实现。def_judge(pos_x,pos_y,block):_iinrange(block.start_pos.Y,block.end_pos.Y+1):ifpos_y+block.end_pos.Y>=BLOCK_HEIGHT:returnFalsefor_jinrange(block.start_pos.X,block.end_pos.X+1):如果pos_y+_i>=0和block.template[_i][_j]!='.'andgame_area[pos_y+_i][pos_x+_j]!='.':returnFalsereturnTrue对接最后一个问题是对接。当方块掉到底部或遇到其他方块时,它就不能再掉了。我把这个叫做“对接”,有名字就更方便了。首先要判断是否可以对接。对接发生后,就是在游戏区域绘制当前方块的非空点。说白了就是将cur_block的非空点按照对应的位置复制到game_area中。并计算一行是否被完全填满,如果完全填满则淘汰。def_dock():nonlocalcur_block,next_block,game_area,cur_pos_x,cur_pos_y,game_overfor_iinrange(cur_block.start_pos.Y,cur_block.end_pos.Y+1):for_jinrange(cur_block.start_pos.X,cur_block.end_pos.X+1):ifcur_block.template[_i][_j]!='.':game_area[cur_pos_y+_i][cur_pos_x+_j]='0'ifcur_pos_y+cur_block.start_pos.Y<=0:game_over=Trueelse:#计算消除remove_idxs=[]for_iinrange(cur_block.start_pos.Y,cur_block.end_pos.Y+1):ifall(_x=='0'for_xingame_area[cur_pos_y+_i]):remove_idxs.append(cur_pos_y+_i)ifremove_idxs:#删除_i=_j=remove_idxs[-1]while_i>=0:while_jinremove_idxs:_j-=1if_j<0:game_area[_i]=['.']*BLOCK_WIDTHelse:game_area[_i]=game_area[_j]_i-=1_j-=1cur_block=next_blocknext_block=blocks.get_block()cur_pos_x,cur_pos_y=(BLOCK_WIDTH-cur_block.end_pos.X-1)//2,-1-cur_block.end_pos.Y至此,整个俄罗斯方块的主要功能就完成了。这里的很多参数是可以调整的,比如旋转不舒服,可以直接调整block的定义,不用改代码逻辑。
