彩色终端

上次介绍了Curses库,这次要深入了解它,关注点是颜色。

考虑到可能有人错过了上一篇文章,下面先进行快速回顾。

接下来创建一个快速、简单的使用颜色的程序,它与之前的程序很像,只是增加了几个新命令。

可以指定很多个颜色对,并在任何需要时加以使用。这需要curses.init_pair函数。语法如下:

curses.init_pair([pairnumber],[foreground color],[background color])

设置颜色时用“Curses.COLOR_(你要想的颜色)”,比如,curses.COLOR_BLUE、curses.COLOR_GREEN等。可选项包括black、red、green、yellow、blue、magenta、cyan和white。

记住,只需要把 “curses.COLOR_”和这些单词的大写形式放在一起。设置好颜色后,就可以把它当作常量在screen.addstr函数中使用了。

语法如下:

myscreen.addstr([row],[column],[text],curses.color_pair(X))

此处的X就是希望使用的颜色。

把下面的程序保存为colortest1.py并执行。记住,不要试图在IDE(包括SPE、Dr.Python等)中运行,要在终端窗口中运行。

import curses
try:
   myscreen = curses.initscr() 
   curses.start_color() 
   curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN) 
   curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) 
   curses.init_pair(3, curses.COLOR_MAGENTA,curses.COLOR_BLACK) 
   myscreen.clear() 
   myscreen.addstr(3,1,"  This is a test  ",curses.color_pair(1)) 
   myscreen.addstr(4,1,"  This is a test  ",curses.color_pair(2)) 
   myscreen.addstr(5,1,"  This is a test  ",curses.color_pair(3))    
   myscreen.refresh() 
   myscreen.getch() 
finally:
   curses.endwin()

此时,你应该能看到灰色背景的窗口中用不同颜色写了3行“This is a test“。第一行是绿底黑字,第二个是白底蓝字,第三行是灰底红字。译者注:Black其实是实际显示为灰。

请注意Try和Finally,这样的组合可以保证无论发生什么情况,程序都可以自动恢复终端窗口的正常状态。还有一种方式,可以使用wrapper命令。

wrapper可以封装所有的工作,包括curses.initscr()、curses.start_color()、curses.endwin() 等,这样就不需要人工干预了。有一点必须牢记,要与main一起使用,它会返回屏幕的指针。

下面是和上面一样的程序,不过使用了 curses.wrapper例程。

import curses
def main(stdscreen):
   curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN)
   curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE)
   curses.init_pair(3, curses.COLOR_MAGENTA,curses.COLOR_BLACK)
   stdscreen.clear()
   stdscreen.addstr(3,1,"  This is a test  ",curses.color_pair(1))
   stdscreen.addstr(4,1,"  This is a test  ",curses.color_pair(2))
   stdscreen.addstr(5,1,"  This is a test  ",curses.color_pair(3))    
   stdscreen.refresh()
   stdscreen.getch()
curses.wrapper(main)

这就简单多了,再也不用担心出错时无法调用curses.endwin()的问题了,所有的工作都会自动完成。

游戏实例

既然已经学习了一大堆的基础知识,那么就把一年来学过东西利用起来,着手开发一个游戏。

开始前,先规划一下。游戏时计算机会随机选一个大写字母。这个字母从右向左移动,并会在某处掉到底部。玩家有一把“枪”,“枪”可用左右键移动,以便在下方追踪字符。敲空格键可以射击字符,在字符滑落前射中它可以得一分,否则,枪会爆炸。失去3把枪游戏就会结束。虽然看起来非常简单,但要写很多代码。

让我们开始吧!首先要进行设置,在深入变成前要先创建几个程序。

首先创建一个工程并取名为game1.py,并输入下面的代码:

import curses
import random
 
class Game1():
 
   def __init__(self):
       pass
   def main(self,stdscr): 
       curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN)
       curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
       curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLUE)
       curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLUE)
       curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_RED)
 
   def StartUp(self):
       curses.wrapper(self.main)
g = Game1()
g.StartUp()

这段代码现在还做不了什么,但它是起点。注意,代码中的前4个颜色对将用于随机显示,而第5个颜色对将用于表示爆炸!接下来,定义游戏所需变量和常量,代码将放在Game1类的“init”方法中,代码如下:

# Line Specific Stuff
      self.GunLine = 22               #Row where our gun lives
      self.GunPosition = 39           #Where the gun starts on GunLine        
      self.LetterLine = 2             #Where our letter runs right to left
      self.ScoreLine = 1              #Where we are going to display the score
      self.ScorePosition = 50         #Where the score column is    
      self.LivesPosition = 65         #Where the lives column is   
      # Letter Specific Stuff
      self.CurrentLetter = "A"        #A dummy Holder Variable
      self.CurrentLetterPosition = 78 #Where the letter will start on the LetterLine
      self.DropPosition = 10          #A dummy Holder Variable
      self.DroppingLetter = 0         #Flag - Is the letter dropping?
      self.CurrentLetterLine = 3      #A dummy Holder Variable
      self.LetterWaitCount = 15       #How many times should we loop before actually working?
      # Bullet Specific Stuff
      self.Shooting = 0               #Flag - Is the gun shooting?
      self.BulletRow = self.GunLine - 1
      self.BulletColumn = self.GunPosition  
      # Other Stuff
      self.LoopCount = 0              #How many loops have we done in MoveLetter
      self.GameScore = 0              #Current Game Score
      self.Lives = 3                  #Default number of lives
      self.CurrentColor = 1           #A dummy Holder Variable
      self.DecScoreOnMiss = 0         #Set to 1 if you want to decrement the
                                      #score every time the letter hits the 
                                      #bottom row

这些变量应该很容易理解,如果在这个阶段感到困惑,没关系,一切会随着不断填补的新代码而变得清晰起来。

离可以运行的程序又进了一步。在它能工作前,仍需要编写一些方法。

下面编一段可以控制字符自右向左移动的代码,详见:http://fullcirclemagazine.pastebin.com/z5CgMAgm

这是程序中最长的方法,它包含了一些新的函数:scrn.delc用于删除指定行列的字符;curses.napms()可以让python休眠一段时间,参数X的时间单位是毫秒。

这段程序的逻辑结构如下所示:

IF we have waited the correct number of loops THEN
  Reset the loop counter
  IF we are moving to the left of the screen THEN
    Delete the character at the the current row,column.
    Sleep for 50 milliseconds
    IF the current column is greater than 2 THEN
         Decrement the current column
    Set the character at the current row,column
    IF the current column is at the random column to drop to the bottom THEN
         Set the DroppingLetter flag to 1
  ELSE
    Delete the character at the current row,column
    Sleep for 50 milliseconds
    IF the current row is less than the line the gun is on THEN
         Increment the current row
         Set the character at the current row,column
    ELSE
         IF 
         Explode (which includes decrementing the score if you wish) and check to see if we continue.
         Pick a new letter and position and start everything over again.
ELSE
  Increment the loopcounter
Refresh the screen.

现在大家应该能理解代码了。接着需要两个新方法来保证一切能运行正常:

代码如下:

def Explode(self,scrn):
       pass
   def ResetForNew(self):
       self.CurrentLetterLine = self.LetterLine
       self.CurrentLetterPosition = 78
       self.DroppingLetter = 0
       self.PickALetter()
       self.PickDropPoint()

为了照应前面的程序,需要再编4个新方法。其中,PickALetter用来随机生成字母,PickDropPoint 用来随机生成字符的下落位置。注意,在系列中曾经简单介绍过random模块。

def PickALetter(self):
      random.seed()
      char = random.randint(65,90)
      self.CurrentLetter = chr(char)
 
  def PickDropPoint(self):
      random.seed()
      self.DropPosition = random.randint(3,78)
 
def CheckKeys(self,scrn,keyin):
      pass
  def CheckForHit(self,scrn):
      pass

在PickALetter中,随机生成65~90间的整数,以对应A~Z。使用random.randint时,应该给出最小值~最大值范围。在 PickDropPoint中也一样要设好范围。

在这两段程序中,都要先调用random.seed()初始化随机数,也就是每次调用时生成不同的随机数种子。第3个方法是CheckKeys【译者注:原文The fourth routine is called CheckKeys有误,应该是第3个,要不然CheckForHit没地摆了】,用来识别用户敲的键,以根据需要移动“枪”。然而这个方法暂不实现,以后用到时再说。同样的,CheckForHit也将在以后予以实现。

紧接着创建名为GameLoop的方法,这个方法是游戏的控制中枢。

def GameLoop(self,scrn):
      test = 1             #Set the loop
      while test == 1:
          curses.napms(20)
          self.MoveLetter(scrn)
          keyin = scrn.getch(self.ScoreLine,self.ScorePosition)
          if keyin == ord('Q') or keyin == 27:  # 'Q' or <Esc>
              break
          else:
              self.CheckKeys(scrn,keyin)
          self.PrintScore(scrn)
          if self.Lives == 0:
              break
      curses.flushinp()
      scrn.clear()

逻辑结构如下:

如果想稍微增加游戏的难度,可要求只有敲了与字符相同的键,枪才能射击。呵呵,这样就成了一个简单的打字游戏。如果这样,Q键就不能作退出键了。

创建NewGame方法,用来新建每一局游戏。其中,nodelay(1)意味着不必一直等着敲键行为的发生,如果发生了,就缓存起来留给下一个处理过程。

def NewGame(self,scrn):
      self.GunChar = curses.ACS_SSBS      
      scrn.addch(self.GunLine,self.GunPosition,self.GunChar,curses.color_pair(2) | curses.A_BOLD)
      scrn.nodelay(1)    #Don't wait for a keystroke...just cache it.
      self.ResetForNew()
      self.GameScore = 0
      self.Lives = 3
      self.PrintScore(scrn)
      scrn.move(self.ScoreLine,self.ScorePosition) 

创建PrintScor方法显示得分和剩余的生命。

def PrintScore(self,scrn):
      scrn.addstr(self.ScoreLine,self.ScorePosition,"SCORE: %d" % self.GameScore)
      scrn.addstr(self.ScoreLine,self.LivesPosition,"LIVES: %d" % self.Lives)

接下来需要在main方法中添加一些代码,以启动游戏过程。附加的代码如下,请放在init_pair的后面。

stdscr.addstr(11,28,"Welcome to Letter Attack")
      stdscr.addstr(13,28,"Press a key to begin....")
      stdscr.getch()
      stdscr.clear()        
      PlayLoop = 1
      while PlayLoop == 1:
          self.NewGame(stdscr)
          self.GameLoop(stdscr)
          stdscr.nodelay(0)
          curses.flushinp()
          stdscr.addstr(12,35,"Game Over")
          stdscr.addstr(14,23,"Do you want to play again? (Y/N)")
          keyin = stdscr.getch(14,56)
          if keyin == ord("N") or keyin == ord("n"):
              break
          else:
              stdscr.clear()

输入后,程序可以运行了。别犹豫,试一试吧!

到此为止,这个程序可以随机生成一个大写字符,把它从右向左随机移动,而且会随机下落。你可能会发现,每次运行程序,第一个字母总是A,而且总是位于第10 列。

这是因为编程时在“init”方法预先设定了。如果想改变,只要在进入main的while循环前,调用self.ResetForNew方法就可以了。

此时,需要处理“枪”了,在Game1类中添加下面的代码:

def MoveGun(self,scrn,direction):
      scrn.addch(self.GunLine,self.GunPosition," ")
      if direction == 0:     # left
          if self.GunPosition > 0:
              self.GunPosition -= 1
      elif direction == 1:  # right
          if self.GunPosition < 79:
              self.GunPosition += 1
      scrn.addch(self.GunLine,self.GunPosition,self.GunChar,curses.color_pair(2) | curses.A_BOLD)

Movegun将把“枪”从现有位置按指定的方向移动。这个方法只用了一个新的方法——addch。在这个方法中,使用了第2个颜色对,同时让表示枪的字符处于加粗状态,这里使用了“或”逻辑运算符“|”。随后实现CheckKeys方法,请用下面的代码替换pass语句。

if keyin == 260: # left arrow -  NOT on keypad
          self.MoveGun(scrn,0)
          curses.flushinp()  #Flush out the input buffer for safety.
      elif keyin == 261: # right arrow - NOT on keypad
          self.MoveGun(scrn,1)
          curses.flushinp()  #Flush out the input buffer for safety.
      elif keyin == 52:  # left arrow  ON keypad
          self.MoveGun(scrn,0)
          curses.flushinp()  #Flush out the input buffer for safety.
      elif keyin == 54:  # right arrow ON keypad 
          self.MoveGun(scrn,1)
          curses.flushinp()  #Flush out the input buffer for safety.
      elif keyin == 32:  #space
          if self.Shooting == 0:
              self.Shooting = 1
              self.BulletColumn = self.GunPosition
              scrn.addch(self.BulletRow,self.BulletColumn,"|")
              curses.flushinp()  #Flush out the input buffer for safety.

创建MoveBullet方法,用于演示子弹向上射出的过程。

def MoveBullet(self,scrn):
      scrn.addch(self.BulletRow,self.BulletColumn," ")
      if self.BulletRow > self.LetterLine:
          self.CheckForHit(scrn)
          self.BulletRow -= 1
          scrn.addch(self.BulletRow,self.BulletColumn,"|")
      else:
          self.CheckForHit(scrn)
          scrn.addch(self.BulletRow,self.BulletColumn," ")
          self.BulletRow = self.GunLine - 1
          self.Shooting = 0

完成前,还需要实现几个方法,下面是CheckForHit和ExplodeBullet所需的代码。

def CheckForHit(self,scrn):
       if self.Shooting == 1:
           if self.BulletRow == self.CurrentLetterLine:
               if self.BulletColumn == self.CurrentLetterPosition:
                   scrn.addch(self.BulletRow,self.BulletColumn," ")                   
                   self.ExplodeBullet(scrn)
                   self.GameScore +=1
                   self.ResetForNew()
 
  def ExplodeBullet(self,scrn):
       scrn.addch(self.BulletRow,self.BulletColumn,"X",curses.color_pair(5))
       scrn.refresh()
       curses.napms(200)
       scrn.addch(self.BulletRow,self.BulletColumn,"|",curses.color_pair(5))
       scrn.refresh()
       curses.napms(200)
       scrn.addch(self.BulletRow,self.BulletColumn,"-",curses.color_pair(5))
       scrn.refresh()
       curses.napms(200)
       scrn.addch(self.BulletRow,self.BulletColumn,".",curses.color_pair(5))
       scrn.refresh()
       curses.napms(200)
       scrn.addch(self.BulletRow,self.BulletColumn," ",curses.color_pair(5))
       scrn.refresh()
       curses.napms(200)

最终,需要实现Explode方法,用下面的代码替换pass语句。

scrn.addch(self.CurrentLetterLine,self.CurrentLetterPosition,"X",curses.color_pair(5))
      curses.napms(100)
      scrn.refresh()
      scrn.addch(self.CurrentLetterLine,self.CurrentLetterPosition,"|",curses.color_pair(5))
      curses.napms(100)
      scrn.refresh()
      scrn.addch(self.CurrentLetterLine,self.CurrentLetterPosition,"-",curses.color_pair(5))
      curses.napms(100)
      scrn.refresh()
      scrn.addch(self.CurrentLetterLine,self.CurrentLetterPosition,".",curses.color_pair(5))
      curses.napms(100)
      scrn.refresh()
      scrn.addch(self.CurrentLetterLine,self.CurrentLetterPosition," ")
      scrn.addch(self.GunLine,self.GunPosition,self.GunChar,curses.color_pair(2) | curses.A_BOLD)
      scrn.refresh()

现在,终于得到一个可以正常工作的程序了。改变LetterWaitCount的值可以加快或减缓字符移动的速度,从而调整游戏的难度;变量 CurrentColor可以用来随机创建新的颜色对选择,可以从4套预先设定的配色方案随机选取字符颜色,可以把颜色分配改为随机色。这留给大家去试一试。

希望大家对此感兴趣,而且愿意增加一些代码以提高游戏的可玩性。一如既往,完整代码可从下面两处获取。