劣质代码评析——《写给大家看的C语言书(第2版)》附录B之21点程序(八)
【重构】(续)
牌的表示:
一副牌有52张,可用一整数数组描述。但是由于在游戏过程中牌数在不断减少,所以用一表示剩余张数的整数和一整数数组共同描述。C99支持一种变量长度数组,但用在这里并没有什么特别的好处,并不合适。
typedef struct { int cards[52]; int num_cards; } POKER ;
参与者:
共两位:庄家(计算机)、人(dealer , player )。
描述它们的数据主要就是得分。根据21点游戏的soft hand规则,A(ACE)可以被视为1点,也可以被视为11点,因此至少要有两个分量来描述参与者。两种情况下的分数分别用low和high描述。最后的有效分数用score描述。
typedef struct { int score; int low ; int high ; } GAMER ;
game_21()函数需要三个变量:一副牌,两个游戏者。
void game_21( void ) { POKER poker; GAMER player = { 0 , 0 , 0 } , dealer = { 0 , 0 , 0 } ; //游戏过程 }
游戏开始,首先对pkr初始化:
init_poker( &poker ); void init_poker( POKER * ); void init_poker( POKER *p_pkr ) { int i ; p_pkr->num_cards = sizeof p_pkr->cards / sizeof p_pkr->cards[0] ;//52 for ( i = 0 ; i < p_pkr->num_cards ; i ++ ){ p_pkr->cards[i] = i % 13 + 1; } }
以保证牌由四套1~13点的牌面组成。这里没写洗牌的过程,而是在后面用随机抽牌的方法来模拟牌面分布的随机性。
p_pkr->num_cards的初值就是52,写成 sizeof p_pkr->cards / sizeof p_pkr->cards[0] 是当时想到了真正的21点游戏的牌可能是由多副52张牌组成的,这个写法可能太一本正经了。按照这个想法POKER类型其实应该这样描述
#define N 1 typedef struct { int cards[52*N]; int num_cards; } POKER ;
不过后来觉得这没什么意义,因为对于单人游戏没必要有那么多牌。所以这个想法并没有贯彻始终。
13是一个Magic Number,不过就这么直接写也不会有什么问题,因为在整个程序中这个常量只出现一次。没用宏描述这个常量的另一个原因是实在想不出应该取什么名。
接下来是轮流抽牌的过程,为了模拟“随机”,在抽牌之前要设置伪随机数的种子。
#include <time.h> srand( ( unsigned )time(NULL) );
time(NULL)的值就是当前时间。不显示地进行类型转换直接用这个值做srand()的实参的写法也有,但本质上是根据实参、形参赋值规则进行隐式的( unsigned )类型转换。
按照规则,庄家首先抽一张牌
puts("庄家拿牌:"); getcard( &dealer , dealcard( &poker ) ); int dealcard( POKER * ); void disp( int ); void getcard( GAMER * , int ); int dealcard( POKER *p_pkr ) { int num = rand() % p_pkr->num_cards ; int card = p_pkr->cards[ num ] ; p_pkr->cards[ num ] = p_pkr->cards[ --p_pkr->num_cards ] ; return card; } void getcard( GAMER *p_plr , int card ) { disp( card ); switch ( card ){ case 1 : p_plr->low += 1 ; p_plr->high += 11 ; break ; default : p_plr->low += card ; p_plr->high += card ; break ; case 11: case 12: case 13: p_plr->low += 10 ; p_plr->high += 10 ; break ; } p_plr->score = p_plr->high > 21 ? p_plr->low : p_plr->high; printf("总分:%d\n",p_plr->score); } void disp( int card ) { switch(card){ case 1 :puts("Ace"); return; default :printf("%d\n",card); return; case 11: puts("Jack"); return; case 12: puts("Queen"); return; case 13: puts("King"); return; } }
其中的dealcard()模拟从poker中随机抽一张牌,然后根据这张牌的点数通过getcard()函数更新dealer的分数。抽牌后调用disp()函数输出了这张牌的牌面。在dealcard()函数中调用disp()函数显示牌面也可以。
接下来,游戏者抽牌。
puts("你拿牌:"); getcard( &player , dealcard( &poker ) ); do{ getcard( &player , dealcard( &poker ) ); }while ( again("继续要牌(Y/N)?") == YES );
由于游戏者第一次抽两张,这里处理为先抽一张,再加上一个do-while语句。写到这里发现,询问游戏者时代继续抽牌的函数与续文游戏者是否继续游戏的函数几乎一致,所以把他们概括为一个函数,并进行了适当修改。
YESNO again( char * ); YESNO again( char *p_message ) { int c; puts( p_message ); c = getchar() ; while ( getchar() != '\n'){ //读完一行 } if ( c=='y' || c == 'Y' ){ return YES; } return NO; }
接下来,庄家继续抽牌,终止条件为点数达到17或17以上。
puts("庄家继续拿牌:"); do{ getcard( &dealer , dealcard( &pkr ) ); }while ( dealer.score < 17 );
最后,宣布胜负:
declare_winner( dealer , player );
这个函数的函数类型声明及定义如下:
void declare_winner( GAMER , GAMER ); void declare_winner( GAMER dealer , GAMER player ) { if ( dealer.score == 21 ){ puts("你输了。"); return ; } if ( dealer.score > 21 ){ if( player.score > 21 ){ puts("平局。"); return ; } } if ( dealer.score < 21 ){ if( player.score > 21 ){ puts("你输了。"); return ; } if( dealer.score >= player.score ){ puts("你输了。"); return ; } } puts("你赢了!\a"); return; }
下面是完整的代码,个别地方进行了微小的修饰性改动。
/* 21点游戏:对《写给大家看的C语言书》附录B之21点程序的重构 */ #include <stdio.h> #include <stdlib.h> #include <time.h> typedef enum { NO , YES, } YESNO ; typedef struct { int cards[52]; int num_cards; } POKER ; typedef struct { int score; int low ; int high ; } GAMER ; YESNO again( char * ); void game_21( void ); void init_poker( POKER * ); int dealcard( POKER * ); void disp( int ); void getcard( GAMER * , int ); void declare_winner( GAMER , GAMER ); int main( void ) { do{ system("CLS"); game_21(); //一轮游戏 }while ( again( "继续游戏(Y/N)?" ) == YES ); system("PAUSE"); return 0; } int dealcard( POKER *p_pkr ) { int num = rand() % p_pkr->num_cards ; int card = p_pkr->cards[ num ] ; p_pkr->cards[ num ] = p_pkr->cards[ -- p_pkr->num_cards ] ; return card; } /* 宣布胜利 */ void declare_winner( GAMER dealer , GAMER player ) { if ( dealer.score == 21 ){ puts("你输了。"); return ; } if ( dealer.score > 21 ){ if( player.score > 21 ){ puts("平局。"); return ; } } if ( dealer.score < 21 ){ if( player.score > 21 ){ puts("你输了。"); return ; } if( dealer.score >= player.score ){ puts("你输了。"); return ; } } puts("你赢了!\a"); return; } /* 计算*p_plr获得card后的分数 */ void getcard( GAMER *p_plr , int card ) { disp( card ); switch ( card ){ case 1 : p_plr->low += 1 ; p_plr->high += 11 ; break ; default : p_plr->low += card ; p_plr->high += card ; break ; case 11: case 12: case 13: p_plr->low += 10 ; p_plr->high += 10 ; break ; } p_plr->score = p_plr->high > 21 ? p_plr->low : p_plr->high; printf("总分:%d\n",p_plr->score); } /* 显示card牌面 */ void disp( int card ) { switch(card){ case 1 :puts("Ace"); return; default :printf("%d\n",card); return; case 11: puts("Jack"); return; case 12: puts("Queen"); return; case 13: puts("King"); return; } } /* 初始化*p_pkr */ void init_poker( POKER *p_pkr ) { int i ; p_pkr->num_cards = sizeof p_pkr->cards / sizeof p_pkr->cards[0] ;//52 for ( i = 0 ; i < p_pkr->num_cards ; i ++ ){ p_pkr->cards[i] = i % 13 + 1; } } void game_21( void ) { POKER poker; GAMER player = { 0 , 0 , 0 } , dealer = { 0 , 0 , 0 } ; init_poker( &poker ); srand( ( unsigned )time(NULL) ); puts("庄家拿牌:"); //庄家取第一张 getcard( &dealer , dealcard( &poker ) ); puts("\n你拿牌:"); //player抽牌 getcard( &player , dealcard( &poker ) ); do{ getcard( &player , dealcard( &poker ) ); }while ( again("继续要牌(Y/N)?") == YES ); puts("\n庄家继续拿牌:"); //庄家继续抽牌 do{ getcard( &dealer , dealcard( &poker ) ); }while ( dealer.score < 17 ); declare_winner( dealer , player ); } YESNO again( char * p_message ) { int c; puts( p_message ); c = getchar() ; while ( getchar() != '\n'){ //读完一行 } if ( c=='y' || c == 'Y' ){ return YES; } return NO;}
(全文完)