用Curses函数编写一个汉诺塔游戏

用Curses函数编写一个汉诺塔游戏
内容:
汉诺塔游戏
Curses的由来
Curses函数介绍
定义数据结构
柱的实现
盘的实现
接收输入的实现
移动盘的实现
编译
关于作者
相关内容:
(1) NEWT程序设计指南
(2) 更多相关文章

 

Curses是为了适应网络上各式各样不同类型的终端,而在UNIX上特别发展出的一套函数库,以专门用来处理UNIX上光标移动及屏幕的显示。本文将为你讲述Curses的编程技巧,并为你提供一个汉诺塔游戏的例程。

汉诺塔游戏
汉诺塔游戏是一种古老的游戏:它有三个木柱和大小各异的盘,这些盘都能套进木柱。木柱的编号分别为1、2和3,要利用2柱把1柱上的盘都放到3柱上为之胜出。移动的规则是这样:每次只能移动柱最上面的一个盘;任何盘都不得放在比它小的盘上。

现在我们用Curses函数在终端上实现这个游戏。

Curses的由来
为了因应网络上各式各样的终端类型,在UNIX上特别发展出一套函数库,专门用来处理UNIX上光标移动及屏幕的显示。Curses最早是由柏克莱大学开发出来的,当时发展此函数库主要原因是为了提高程序对不同终端的兼容性。利用Curses开发出来的程序是与终端无关的,这对于网络上的程序是一件相当重要的事。Curses将所有终端的资料存放在一个资料库中,有了这些记录,Curses程序就能知道遇到的是哪一种终端。

Curses函数介绍
下面介绍一些常用的Curses函数。

WINDOW *initscr(void)
initscr()函数通常是Curses程序第一个调用函数,用于判断终端类型和初始化Curses数据结构,同时也对终端进行一次刷新以清除屏幕,为以后的操作做准备。若执行时发生错误则在标准错误输出中输出错误信息并退出,正确执行就返回stdscr这一属于WINDOW类型的变量。

int endwin(void)
每一个Curses程序在退出或要临时脱离Curses模式时都应调用endwin()函数。调用这个函数将恢复tty终端原来的状态,把光标移到屏幕的左下角,重置终端为正确的非虚拟模式.

bool has_colors(void)
has_colors()例程无需参数。若终端能产生彩色,则返回TRUE;否则返回FALSE。程序员通过调用它去判断终端是否可以使用彩色。

int use_default_colors(void)
use_default_colors例程是Curses库的一个扩展,它告诉Curses库把终端设置为缺省的前景/背景,缺省时前景为白色,背景为黑色.成功设置返回OK,否则返回ERR,两者都是整型数值。

int init_pair(short pair, short f, short b)
init_pair()例程用于更改一个彩色对的定义。彩色对是Curses的一个概念,它用一个整型数值去标志一对前景/背景彩色。该例程有三个参数:彩色对数值pair,其范围从1到 COLOR_PAIRS-1; f(foreground)指定前景彩色; b(background)指定背景彩色。Curses预定义的彩色有:

  • COLOR_BLACK-黑色
  • COLOR_WHITE-白色
  • COLOR_RED-红色
  • COLOR_GREEN-绿色
  • COLOR_YELLOW-黄色
  • COLOR_BLUE-蓝色
  • COLOR_MAGENTA-洋红
  • COLOR_CYAN-青色
若你嫌这几种彩色太单调,还可用init_color()来自定义。

int echo(void)
用于回显从键盘输入的字符

int noecho(void)
不在终端中显示从键盘输入的字符

int beep(void)
向终端发出一个声频警告,成功执行则返回OK,否则返回ERR

int attrset(int attrs)

attrset例程把当前窗口设置为参数attrs所指定的属性,其中常用的有:

  • A_UNDERLINE--加底线

  • A_REVERSE--反白

  • A_BLINK--闪烁

  • A_BOLD--高量度

  • A_MORMAL--标准模式,也用于恢复


int refresh(void)
当在终端上进行任何操作或修改后,所期望的效果只有在调用refresh()后才能显现出来。

int mvprintw(int y, int x, char *fmt [, arg] ...)
mvwprintw()用于在屏幕上y行x列的指定位置输出一格式化的字符串,格式化用法与printf()相同。

int mvaddnstr(int y, int x, const char *str, int n)
在屏幕指定位置输出一字符串,与上述例程相比只缺少格式化输出而已。

int mvaddch(int y, int x, chtype ch)
在屏幕指定位置输出一个字符。

int clrtoeol(void)
删除当前行中从光标到行末的的所有字符

int erase(void)
在屏幕的各个位置复制空白以使屏幕得到清除。

事实上,Curses所提供的函数不下200个,但并没有必要把所有的函数都一一掌握,一切再复杂的函数均可以用一些基本的函数组合而成。Curses函数学得太多,反而有反效果,徒增加自己的困扰。

有了上述的基础,现在就进行实质性的工作。

定义数据结构
把编号为1、2和3的三条柱相比较,发现它们有相同和不同之处:当某一盘均放在所有柱上时,若不理会各柱的编号,将很难分辨这些柱,它们都是相同的,因此如果有一数据结构能描述其中一条柱,那么这一数据结构都适用于其余的柱;由游戏规则可知,一个盘只能在一条柱上,各条柱因有不同的盘而不同,柱上的盘应是该柱的属性,因有它从而能从其他柱中区别出来。根据上述分析,抽象出用于描述柱的数据结构如下:

struct Peg { size_t Length[MAXTILES];  /*自底而上,存放盘的大小,盘的大小应该是各不同的*/ int Count; /*记录柱上的盘的总数*/ }; 
定义数据结构是第一步,也是至关重要的一步。

柱的实现
要在终端上画出文章开头那幅图,使用Curses函数其实并不难。

画出水平直线的算法:

  1. 设置前景/背景彩色对,
  2. 从水平直线始端到水平直线末端循环执行mvaddch()输出空格,便得到一水平彩色条。


画出垂直直线的算法与上述算法基本相同,只是从垂直直线始端到垂直直线末端循环执行。

三条柱的实现:

 for (Line = TOPLINE; Line < BASELINE; Line++) { mvaddch(Line, LEFTPEG, ' '); mvaddch(Line, MIDPEG, ' '); mvaddch(Line, RIGHTPEG, ' '); } 
其中LEFTPEG,MIDPEG,RIGHTPEG分别是三条柱的位置,垂直长度为从TOPLINE 到BASELINE

盘的实现
为了实现盘堆叠起来时呈尖塔形,盘的长度要形成等差,例如1,3,5,7……。

在画出盘前必需设置struct Peg结构。

 static struct Peg Pegs[NPEGS]; /*为三条柱定义三个结构*/ for (Size = NTiles * 2 + 1, SlotNo = 0; Size >= 3; Size -= 2) Pegs[0].Length[SlotNo++] = Size; /*自底而上设置每个盘,Ntiles为柱上盘总数*/ Pegs[0].Count = Ntiles;/*设置柱上盘总数*/ 


然后,就可以画出三条柱上的盘:

for (peg = 0; peg < NPEGS; peg++) { /*分别画出三条柱上的盘,NPEGS为三*/ for (SlotNo = 0; SlotNo < Pegs[peg].Count; SlotNo++) { memset(TileBuf, ' ', Pegs[peg].Length[SlotNo]); TileBuf[Pegs[peg].Length[SlotNo]] = '/0'; /*设置盘的长度,即空格串长度*/ if (has_colors()) attrset(COLOR_PAIR(LENTOIND(Pegs[peg].Length[SlotNo]))); else attrset(A_REVERSE); /*设置彩色*/ mvaddstr(BASELINE - (SlotNo + 1), (int) (PegPos[peg] - Pegs[peg].Length[SlotNo] / 2),TileBuf); /*画出盘*/ } } 


接收输入的实现
Curses有一个getch()例程是专门用来捕获键盘输入,返回的是所捕获的字符。

移动盘的实现
实现盘的移动前首先要判断输入是否符合游戏规则。移动的目标是柱上最顶层的盘,输入是有效的柱的编号。假定From起始柱,To终点柱,必须符合以下要求:

  1. 任何盘都不得放在比它小的盘上,即其长度小于终点柱最顶层的盘的长度。
    Pegs[From].Length[Pegs[From].Count - 1] >Pegs[To].Length[Pegs[To].Count - 1]
  2. 输入应是有效的,包括From和To都是柱的编号,在1至3之间;From不等于To,这是没意义的。


符合上述要求的输入,被认为是有效,接着就实现盘的移动。盘的移动实际上是改变标志柱状态的数据结构。首先,要改变起始柱状态:Pegs[From].Count减一,因Length数组是有底而上设置的,当Count减一时柱最顶层的盘就自动丢弃;然后,把目标盘加入到终点柱上,即 Pegs[To].Length[Pegs[To].Count] = Pegs[From].Length[Pegs[From].Count];
Pegs[To].Count++;

再根据数据结构重画一次屏幕。

给出完整的程序如下:

/************************************************* * 正常运行: hanoi -n <步数> * * 示例: hanoi -a <步数> * * 帮助: hanoi -h * *************************************************/ #include <curses.h> #include <string.h> #include <unistd.h> #define EXIT_SUCCESS 0 #define EXIT_FAILURE 1 #define NPEGS 3 #define MINTILES 3 #define MAXTILES 6 #define DEFAULTTILES 7 #define TOPLINE 6 #define BASELINE 16 #define STATUSLINE (LINES-3) #define LEFTPEG 19 #define MIDPEG 39 #define RIGHTPEG 59 #define LENTOIND(x) (((x)-1)/2) #define OTHER(a,b) (3-((a)+(b))) struct Peg { size_t Length[MAXTILES]; int Count; }; static struct Peg Pegs[NPEGS]; static int PegPos[] = {LEFTPEG, MIDPEG, RIGHTPEG}; static int TileColour[] = { COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN, }; static int NMoves = 0; static void InitTiles(int NTiles); static void DisplayTiles(void); static void MakeMove(int From, int To); static void AutoMove(int From, int To, int Num); static void Usage(void); static int Solved(int NumTiles); static int GetMove(int *From, int *To); static int InvalidMove(int From, int To); int main(int argc, char **argv) { int NTiles, FromCol, ToCol,opt; unsigned char AutoFlag = 0; if(argc<2 || argc>3){ Usage();exit(0);} while( (opt=getopt(argc,argv,"a:n:h") )!=-1) switch (opt) { case 'n': NTiles = atoi(optarg); if (NTiles > MAXTILES || NTiles < MINTILES) { fprintf(stderr, "步数从 %d 自 %d之间/n", MINTILES, MAXTILES); return EXIT_FAILURE; } AutoFlag = FALSE; break; case 'a': NTiles = atoi(optarg); if (NTiles > MAXTILES || NTiles < MINTILES) { fprintf(stderr, "步数从 %d 自 %d之间/n", MINTILES, MAXTILES); return EXIT_FAILURE; } AutoFlag = TRUE; break; case 'h': case '?': case ':': Usage(); exit(0); } initscr(); if (has_colors()) { int i; int bg = COLOR_BLACK; start_color(); if (use_default_colors() == OK) bg = -1; for (i = 0; i < 9; i++) init_pair(i + 1, bg, TileColour[i]); } cbreak(); /*LINES和COLS是Curses内部变量,用于存储当前终端的行数和列数*/ if (LINES < 24 || COLS < 80) /*标准的终端为24行80列*/ { endwin(); fprintf(stderr, "当前小于 24x80 /n"); return EXIT_FAILURE; } if (AutoFlag) { curs_set(0); leaveok(stdscr, TRUE); } InitTiles(NTiles); DisplayTiles(); if (AutoFlag) { do { noecho(); AutoMove(0, 2, NTiles); } while (!Solved(NTiles)); sleep(2); } else { echo(); for (;;) { if (GetMove(&FromCol, &ToCol)) break; if (InvalidMove(FromCol, ToCol)) { mvaddstr(STATUSLINE, 0, "移 动 无 效 !!"); refresh(); beep(); sleep(2); continue; } MakeMove(FromCol, ToCol); if (Solved(NTiles)) { mvprintw(STATUSLINE, 0, "恭喜!! 恭喜!! 你用 %d 步赢了", NMoves); refresh(); sleep(5); break; } } } endwin(); return EXIT_SUCCESS; } static int InvalidMove(int From, int To) { if (From >= NPEGS) return TRUE; if (From < 0) return TRUE; if (To >= NPEGS) return TRUE; if (To < 0) return TRUE; if (From == To) return TRUE; if (!Pegs[From].Count) return TRUE; if (Pegs[To].Count && Pegs[From].Length[Pegs[From].Count - 1] > Pegs[To].Length[Pegs[To].Count - 1]) return TRUE; return FALSE; } static void InitTiles(int NTiles) { int Size, SlotNo; for (Size = NTiles * 2 + 1, SlotNo = 0; Size >= 3; Size -= 2) Pegs[0].Length[SlotNo++] = Size; Pegs[0].Count = NTiles; Pegs[1].Count = 0; Pegs[2].Count = 0; } static void DisplayTiles(void) { int Line, peg, SlotNo; char TileBuf[BUFSIZ]; erase(); init_pair(20,COLOR_MAGENTA,COLOR_BLACK); attrset(COLOR_PAIR(20)|A_BOLD); mvaddstr(1, 25,"汉 诺 塔 游 戏"); attrset(A_NORMAL); mvprintw(18, 30, "当前步数 : "); init_pair(21,COLOR_RED,COLOR_BLACK); attrset(COLOR_PAIR(21)|A_BOLD); mvprintw(18,41,"%d",NMoves); attrset(A_NORMAL); attrset(A_REVERSE); mvaddstr(BASELINE, 8, " "); for (Line = TOPLINE; Line < BASELINE; Line++) { mvaddch(Line, LEFTPEG, ' '); mvaddch(Line, MIDPEG, ' '); mvaddch(Line, RIGHTPEG, ' '); } mvaddch(BASELINE, LEFTPEG, '1'); mvaddch(BASELINE, MIDPEG, '2'); mvaddch(BASELINE, RIGHTPEG, '3'); attrset(A_NORMAL); for (peg = 0; peg < NPEGS; peg++) { for (SlotNo = 0; SlotNo < Pegs[peg].Count; SlotNo++) { memset(TileBuf, ' ', Pegs[peg].Length[SlotNo]); TileBuf[Pegs[peg].Length[SlotNo]] = '/0'; if (has_colors()) attrset(COLOR_PAIR(LENTOIND(Pegs[peg].Length[SlotNo]))); else attrset(A_REVERSE); mvaddstr(BASELINE - (SlotNo + 1), (int) (PegPos[peg] - Pegs[peg].Length[SlotNo] / 2), TileBuf); } } attrset(A_NORMAL); refresh(); } static int GetMove(int *From, int *To) { attrset(A_REVERSE); mvaddstr(LINES-1, 20,"<Q>/<q> 退出 <1>-<3> 移动"); attrset(A_NORMAL); mvaddstr(STATUSLINE, 0, "下一步: 从 "); clrtoeol(); refresh(); *From = getch(); if ((*From == 'q')||(*From == 'Q') )return TRUE; *From -= ('0' + 1); addstr(" 到 "); clrtoeol(); refresh(); *To = getch(); if ((*To == 'q') || (*To == 'Q'))return TRUE; *To -= ('0' + 1); refresh(); napms(500); move(STATUSLINE, 0); clrtoeol(); refresh(); return FALSE; } static void MakeMove(int From, int To) { Pegs[From].Count--; Pegs[To].Length[Pegs[To].Count] = Pegs[From].Length[Pegs[From].Count]; Pegs[To].Count++; NMoves++; DisplayTiles(); } static void AutoMove(int From, int To, int Num) { if (Num == 1) { MakeMove(From, To); napms(500); return; } AutoMove(From, OTHER(From, To), Num - 1); MakeMove(From, To); napms(500); AutoMove(OTHER(From, To), To, Num - 1); } static int Solved(int NumTiles) { int i; for (i = 1; i < NPEGS; i++) if (Pegs[i].Count == NumTiles) return TRUE; return FALSE; } static void Usage() { fprintf(stderr, "/nhanoi/t[n]步数 -- 玩游戏/n/ /t[a]步数 -- 示范/n/t[h] /t-- 帮助 /n"); printf("/e[0;33m其中步数在%d-%d之间/e[0m/n", MINTILES, MAXTILES); } /*----------------the end-----------------*/  


编译
在Linux上编译Curses程序时,gcc要带 -lcurses参数,即
gcc -o hanoi hanoi.c -lcurses

运行结果如下图:

运行结果如下图


作者简介
梁俊辉,对Linux的网络应用和程序设计有浓厚兴趣,并且专注于这一方面研究,在IBM developerWorks--Linux专区上发表过多篇文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值