*** 從 NIM 遊戲中複習亂數的使用技巧與認識 xor 運算的神奇 Task: 練習 寫一個 NIM 遊戲, 電腦與人對玩, 且電腦要儘可能贏 ! Purpose: To understand: (a) 再熟析 使用 Pseudo Random Number Generator(PRNG;亂數產生器) :-) (b) 再思考如何才能 "儘可能贏" ? (c) 如何善用 xor 運算以便實現 "儘可能想辦法贏" ? Hints: 請回頭看 給大家的 BATNUM game 是如何的 "儘可能贏" ? 關於 NIM game, 請參看 http://en.wikipedia.org/wiki/Nim 或是請自己用 http://gogle.com 打入 "nim" 查詢 註: 關於"亂數"可用 http://gogle.com 打入 "PRNG wiki" 查詢 xor 運算用途很多: 繪圖經常用 xor 方式畫上去, 這樣才能 "擦掉" ! (再 xor 一次) 加密解密也是很長用 xor, 因為簡單又快速 ! *** *** =================================================*****/ /** 大事化小, 小事化無! 上司管下司, 鋤頭管畚箕 ! ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^ 再次提醒, 把握 "大哥叫小弟做事" 的原則, 或是 "立委包工程分包出去" 的作法! 把工作作切開, 分給一個個的小弟函數(function, 函式), 然後大哥(main program)要做的就指是把各小弟組織起來就完成了! 當然 , Java 中還要注意全體須住在 class 內 ! 這個 NIM 是兩人對玩的遊戲, 與 BATNUM game 很像! 應該說, 只要是兩方對玩的 Game 都很像 , 阿就輪流 (take turn) 啊 ! *** 注意, 這與上週的 Bulls and cows 反而比較不像, 因為 Bulls and Cows 是一個人想, 讓另一個人猜, 雖然也可以寫成互相猜對方的數, 但是也可以只是單方猜 :-) 可是 NIM 與 BATNUM 都是兩人對戰輪流拿的遊戲, 只是 BATNUM 只有一堆, 至少拿一個, 至多拿幾個要先約定好, 但 NIM 是至少兩堆, 為免太難玩, 建議最多 七堆, 也是 至少拿一個, 但至多可以把整堆拿完, 不可跨堆拿! NIM 與 BATNUM 同樣也可以事先規定拿到最後一個的贏或是輸! *** NIM 的必贏法: 把各堆石頭數都化成二進位, 然後每位獨立相加不進位, 例如: (此即 xor) 1 0 1 1 = 11 1 0 1 0 = 10 0 1 1 1 = 7 ------------------------- 0 1 1 0 <== xor sum, 又稱 NIM sum (註: 因電腦本來就是二進位, 所以只要直接把各數 xor ( ^ ) 就可!) 接著, 想辦法從一堆中拿走若干個, 使其每位獨立相加不進位之結果都是 0 只要 xor sum 不是 0, 那一定至少可以找到一堆可以拿了會贏的 ! 就是與 xor sum 的最左的 1 那位(bit) 同樣是 1 的堆都可! 拿多少個呢? 拿了後要讓它們的 xor sum (NIM sum) 是 0 啦 Hint: 拿走 該堆數 - (該堆數 xor (它們的xor sum)) ** 當然, 若輪你時xor sum 已是 0, 那你輸定了, 就從任一堆拿一個拖延:-) 這招適用於拿到最後一個的贏或是輸都可以! (神奇吧?) 不過若拿最後一個的是輸, 則萬一照此法拿完後每堆都最多一個, 則須稍作修正! 也就是此時須拿成奇數堆個 1 才會贏, 因為拿最後一個的輸! 修正的方法可能是多拿一個或是少拿一個: 若該堆是拿成 0 阿就放一個回去, 若該堆是還剩一個就連那個一起拿走 ! ******** ******* ******** ******** ******** ******** Stepwise Refinement 的觀念是以下這位程式語言大師提出的: 尼可拉斯.凱吉 .. ㄚ不是, 應該是 尼可拉斯.魏斯 (Niklaus Wirth) 尼可拉斯.魏斯 (Niklaus Wirth) 是 Pascal Language 的發明人 ! 他的書 "Algorithms + Data Structures = Programs" 有很長一陣子 是各大學 資料結構的教科書 ! (Wirth 是 瑞士計算機科學家) Dec-CPP 的整合環境(IDE)就是用 Pascal (Delphi)寫的 :-) ******************************************** *** 11th week ******/ "Program Development by Stepwise Refinement," by Niklaus Wirth; 這篇 1971年 4月發表於 CACM 期刊 的文章一發表就引起廣大的共鳴! ____________________________________________________________________ ** You should develop your program using stepwise refinement(逐步改進) approach. That is to say, write a simple version first and modify it later on. Stepwise refinement approach is a good methodology for programming. The objective of this design methodology is to first establish the overall structure and the relationships between the various parts of the problem, and then address the specific and complex issues of the implementations of the various sub-parts (functions). ** But you should write comments in your program as early as possible! ** ==> 但註解不可以等事後再補寫, 應該一邊寫程式就一邊寫! 以這 NIM 來說, 就是說 userTurn( ) 先寫不考慮防呆機制的簡易版, 且 computerTurn( ) 也先不管必贏的, 能正常玩再說! (假設 User 守規矩) 然後, 再逐步把userTurn( )改為可以防呆, 並讓computerTurn( )可以儘量贏! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NIM: from Wikipedia.org Nim is a two-player mathematical game of strategy in which players take turns removing objects from distinct heaps. On each turn, a player must remove at least one object, and may remove any number of objects provided they all come from the same heap. The name "NIM" is probably derived from German nimm! which is meaning "take!", or the obsolete English verb nim of the same meaning. It should also be noted that rotating the word NIM by 180 degrees results in "WIN" :-) There is an easily calculated way to determine which player will win and what winning moves are open to that player. The key to the theory of the game is the binary digital sum of the heap sizes, that is, the sum (in binary) neglecting all carries from one digit to another. This operation is also known as "exclusive or" (xor). Within combinatorial game theory it is usually called the nim-sum. The nim-sum of x and y is written as x ♁ y to distinguish it from the ordinary sum, x + y. In normal NIM game, the player taking the last object is the winner. The winning strategy is to finish every move with a Nim-sum of 0, which is always possible if the Nim-sum is not zero before the move. If the Nim-sum is zero, then the next player will lose if the other player does not make a mistake. As a particular simple case, if there are only two heaps left, the strategy is to reduce the number of objects in the bigger heap to make the heaps equal. After that, no matter what move your opponent makes, you can make the same move on the other heap, guaranteeing that you take the last object and that you win the game. If the rule is changed to say that the one who takes the last object is to lose, then the winning strategy is different ONLY when the normal play move would leave no heap of size 2 or larger. In that case, the correct move is to leave an odd number of heaps of size 1 (in normal NIM play, the correct move would be to leave an even number of such heaps). // NIM Game 電腦必贏的密技 --- // 參考 wikipedia.org //####################### // assume that the global variable nPile is the #of pile in this game. // assume that nStone[1] .. nStone[nPile] are 各堆石頭數 // 注意第0堆不用, 浪費一個元素沒關係! 這會方便許多, why ? // #define MAX_PILE 7 // int nPile = 3 + rand( )%(MAX_PILE -3 + 1); // 3..7 // 共有 nPile 堆石頭, // 各堆石頭數在 array int nStone[ ] , 注意我們浪費第 0 元素不用 // C/C++: int nStone[MAX_PILE + 1]; // 第 0 堆不用, 所以要多一堆 // Java: int nStone[ ] = new int [MAX_PILE + 1]; // 第 0 堆不用 ////// int findPile(int sumXOR); // function to find the available pile int atMostOne(int p, int n); // to check if we reach to the critical step // this function is ONLY useful for the one takes last object to lose // i.e., check to 看看若從第 p 堆拿走 n 個後是否造成各堆最多一個? void computerTurn( ) { // 把 array 多留一個元素, 第0個不要用 int pp, nn, remain, i; // 注意不是 0..n-1, 我們用 1..n 堆! int sum = 0; for(i=1; i <= nPile; ++i) sum ^= nStone[i]; // 不用第 0 堆! if(sum == 0) { nn = 1; // 因 XOR 起來是0, 表示必輸的, 隨便拿一個等對方拿錯 // ..從任一有石頭的堆(pp) 拿走一個 (nn = 1); // .. (用 while 找 pp, 很簡單, 留給 同學們 想 :-) // ..或從最多石頭那堆拿一個更好! } else { // 會贏, 但是要找出如何拿.. pp = findPile(sum); // find which pile that we can win in normal NIM // 找出該拿哪一堆才會贏, 先用拿最後一個會贏的策略 remain = nStone[pp] ^ sum; // 要拿成剩下 remain 這麼多 nn = nStone[pp] - remain; // 算出該拿 nn 這麼多才會使 sum 是 0 //special process for last object to lose, 若拿最後是輸, 要特別處理 if( ( ! lastToWin) && atMostOne(pp, nn) ) { if(remain==1) ++nn; //還有, 拿光它,把剩下的那個也拿走;例 1,1,1,6 if(remain==0) --nn; //放回一個!Why?例 1,1,5 會誤判要拿成 1, 1, 0 } // if lastToWin== false 表示拿到最後的是輸 }//if // 從 pp 堆拿走 nn 個, 阿就該堆減去 nn 啦 printStatus( ); // tell user 目前各堆的狀況.. } int findPile(int sum) { // 找 #of objects 與 sum 做 xor 後會變少的堆 int i=1; for(i=1; i<=nPile; ++i) // 從第 1 堆找到第 nPile 堆 if( (sum ^ nStone[i] ) < nStone[i]) return i; // XOR 後變小表示可 // impossible to here -- 因不可能找不到任何一堆! wikipedia.org 有證明 fprintf(stderr, " ? 有問題喔! impossible to here!\n"); return -1; // 故意回傳 -1 } // findPile int atMostOne(int p, int n) { // 看看從第 p 堆拿走 n 個會不會每堆最多一個? //若從第 p 堆若拿走 n 個後, 沒有一堆超過一個石頭, 則傳回 1 表示 yes int i; int ans = 1; // assume yes; 也可像我之前範例使用 true/false nStone[p] -= n ; // 注意是 減等於 (-=) , 先假裝第 p 堆拿走 n 個 for(i=1; i<=nPile; ++i) // check all piles (heaps) 1..nPile if(nStone[i] > 1) ans = 0; // at least one pile has stones > 1 nStone[p] += n ; // 把 n 個放回第 p 堆! 因我們只是檢查 return ans; // 若要用 tru/false, 函數開始須 bool ans = true; } // atMostOne in each pile (heap) 01 //nim0.c --- NIM GAME , prototype by tsaiwn@csie.nctu.edu.tw 02 /********** 03 * 這個習題 NIM 遊戲對戰, 請用這程式修改, 原有 statements 儘量留著! 04 * 電腦要儘可能贏, 就是說能贏就要贏! 05 * 本練習題規定: 06 * 產生的 NIM 石頭的堆數限制在 3..7, 每堆石頭數限在 5..31, 亂數決定 07 * 誰先拿則 問 user 決定; 請寫成讓人與電腦交談對玩! 08 * ! 請注意本程式 pile 堆的編號 0 不用, 從第一堆用起! 09 *******************************************************/////////// 10 /// 若要用 Turbo C++ 的分割劃面可以參考: 11 /// ftp://ftp.csie.nctu.edu.tw/pub/CSIE/course/cs2/textwin.cpp 12 #include 13 #include 14 #include 15 16 #ifndef __cplusplus 17 //typedef int bool; // 與 #define bool int 同效果, 但要加分號; 18 typedef enum {false=0, true } bool; // 這招 更好 :-) 19 #endif 20 21 #define MAX_PILE 7 22 #define MAX_STONE 31 23 #define MIN_STONE 5 24 25 /// =================> use Global variables 比較方便但要小心處理! 26 int nPile; /* number of Pile */ 27 int nStone[MAX_PILE + 1]; // 第 0 堆不要用, 用 1 到 7 !! 28 // 注意不用第 0 堆是因這樣跟人的習慣比較像 :-) 29 int pTake, nTake; // take from which pile, how many stones 30 int lastToWin; // 1 == 拿到最後一個贏, 0 == 拿到最後一個贏輸 31 int userFirst; // user want to Go first 32 int gameBegin; // used in printStatus( ) 33 34 char msgWin[ ][33] = { "拿到最後一顆輸Loss!", "拿到最後一顆贏Win!" }; 35 bool gameOver( ); // indicate somebody took the last stone 36 void userTurn(void), computerTurn(void); 37 void prepareGame(void); // 把game 用到的整體(global;共用)變數準備好. 38 void playGame(void); 39 void printStatus(void); // 印出各堆石頭狀況. 40 void hello(void) { 41 printf("NIM 是從 many多石堆中輪流拿, 只能從一堆拿, 至少拿一個,\n"); 42 // 有些compiler下不能寫 "許" ? compile時會出問題! 故寫 many 多 :-) 43 printf("..NIM 每次只能從一堆拿, 至少拿一個,\n"); 44 printf("..至多把該堆拿光, 可規定拿到最後贏, 或拿到最後顆輸!\n\n"); 45 } 46 int askYN( ){ 47 int ans; // 1 == yes, 0 == no 48 static char buf[333]; 49 fgets(buf, sizeof(buf), stdin); 50 if(buf[0] == 'N' || buf[0] =='n') return 0; 51 if(buf[0] == '0' ) return 0; 52 return 1; // We like the user to play :-) So default is YES 53 } 54 int main( ) { 55 int playAgain; // Local variable is enough 56 hello( ); 57 srand( time(0) &0xfffff ); // true random, see "man 3 time" 58 playAgain = 1; // force to play :-) 59 while(playAgain) { 60 prepareGame( ); 61 playGame( ); 62 printf("Want to play again (Y, N)? "); 63 playAgain = askYN( ); 64 } 65 printf("\nThank you for your play!\nCome to play again!\n"); 66 return 0; 67 } // main 68 69 70 /////////////////////////////////////////////////////////// 71 void prepareGame(void) { 72 int i; 73 lastToWin = userFirst = (38==38); // true 74 if(rand( ) % 100 >= 50) lastToWin = 0; // 50%, 就是 %2 == 1 75 nPile = 3 + rand( ) % (MAX_PILE-3+1); // 3..7 76 for(i=1; i <= nPile; ++i) { /// 注意由第一堆算起 77 nStone[i] = MIN_STONE + rand( ) % (MAX_STONE -MIN_STONE +1); 78 } 79 } // prepareGame 80 void printStatus( ){ 81 int i; 82 printf("There are 共有 %d pile堆石頭:\n", nPile); 83 printf(" P_%d: %d", 1, nStone[1]); // Note that 沒第 0 堆 84 for(i=2; i <= nPile; ++i) { // then pile-2, pile-3, ... 85 printf(", P_%d: %d", i, nStone[i]); 86 } 87 printf("\n"); 88 if(gameBegin) { // 只有剛開始才印這句 89 printf("!!! 注意 %s! %s!!\n", 90 msgWin[lastToWin], msgWin[lastToWin]); 91 gameBegin = 0; //print only when game begins,do NOT print again 92 } 93 } // printStatus 94 bool gameOver( ) { // Note that Pile# is 1..nPile 95 int i, totalStone=0; 96 for(i=1; i <= nPile; ++i) totalStone += nStone[i]; 97 return (totalStone <= 0); // no more stone == gameOver 98 } 99 void computerTurn( ){ 100 pTake = 1; // 從第一堆看看 101 while(nStone[pTake]==0) ++pTake; // 目前先找還有石頭的 102 nTake = 1; // 至少要拿一個 103 nTake = 1 + rand( ) % nStone[pTake]; // 隨便拿 104 // 105 // 要確保資料正確, 不可拿超過, 並想辦法能贏就要贏 106 // how to win? 107 // 108 printf(" --> I take %d from Pile %d\n", nTake, pTake); 109 nStone[pTake] -= nTake; 110 printStatus( ); 111 } // computerTurn 112 void userTurn( ) { 113 static char buf[88]; 114 again: 115 printf("Take from which Pile? "); 116 fgets(buf, sizeof(buf), stdin); 117 pTake = (int)atol(buf); 118 // 防呆處理: 要確保輸入資料正確, 不可讓 user 亂輸入.. 119 // ... 若該堆 0 個也不要讓他拿喔! 120 printf(" from Pile %d, take how many stones (1..%d)? ", 121 pTake, nStone[pTake]); 122 fgets(buf, sizeof(buf), stdin); 123 nTake = (int)atol(buf); 124 if(nTake < 0) goto again; // 允許他後悔改從別堆 Pile 拿 :-) 125 // 防呆處理: 要確保輸入資料正確, 不可拿超過 126 ///*** how ? 127 nStone[pTake] -= nTake; 128 printStatus( ); 129 } // userTurn 130 void playGame(void) { 131 int userWin; // Local variable is enough 132 gameBegin = 1; // 每次game開始要強迫印出拿最後一個會贏還是會輸 133 printStatus( ); // 印出目前各堆石頭, 若第一次印要印上列說的 134 printf(" ..Want to go first (Y, N) ? "); 135 userFirst = askYN( ); 136 userWin = 0; 137 if(userFirst) userTurn( ); 138 if(gameOver( ) && lastToWin) userWin = 1; 139 while( !gameOver( ) ) { 140 computerTurn( ); 141 if(gameOver( )) { 142 if(lastToWin) userWin = 0; else userWin = 1; 143 break; 144 } 145 userTurn( ); 146 if(gameOver( )) { 147 if(lastToWin) userWin = 1; else userWin = 0; 148 break; 149 } 150 } // while 151 if(userWin) { 152 printf(" Congratulations -- You Win!\n"); 153 }else{ 154 printf(" SOOORRRY! -- Ha Ha Ha! -- I Win!\n"); 155 } 156 } // playGame 157 /************ 158 -- 159 Proof techniques #1: Proof by Induction. 160 Q: 試用歸納法證明:飯永遠吃不飽. 161 1. 吃一粒米顯然不會飽 162 2. 假設吃了 n 粒米沒有飽 163 再吃一粒米顯然也不會飽, 就是說吃 n+1 粒米也不會飽 164 由此可推得米飯永遠吃不飽 ! 165 QED. (QED translates from the Latin as "So what?") 166 ((((((((((((((((((((((((((())))))))))))))))))))))))))))) 167 ********************************************************/ 168 01 //nim2.c --- NIM GAME, modified by tsaiwn@csie.nctu.edu.tw 02 //prototype by tsaiwn@csie.nctu.edu.tw 03 // 你該有注意到在前一版本中, 多處用 int 的 0 與 1 表示 false 與 true; 04 // 既然可用 bool, false, true, 第0版中只用在 bool gameOver( )很可惜! 05 // ..這版本就把該用 bool, false, true 的都儘量用! 06 // C++ 本來就認識 bool, false, true; C則可用 typedef enum...bool; 07 // 其實更好的是 typedef {false, true} bool; (稍後解說) 08 //... 還有, 這版本也做了適度的防呆, 但還有改進的空間 09 /********** * ! 請注意本程式 pile 堆的編號 0 不用, 從第一堆用起! 10 *******************************************************///////// 11 /*** === === === 關於 false, true, bool (Java 中稱 boolean) 12 在 BATNUM Game 範例中, 13 我們用 #define bool int 使得 compiler 看到 bool 就換成 int, 14 並且利用 C/C++ 是把 false 看作 0, 把 true 看作 1 的特性, 15 在程式中我們充份利用 bool 使得程式更有可讀性! 16 ======> 其實這些在 C++ / Java /C# 都已經這樣 (注意 bool, boolean) 17 ** 其實, 在 C 語言中, 除了 #define bool int 18 還可以用 typedef int bool; 使得 compiler 認得 bool 19 注意, #define 是 C 的前處理器的句子, 不是 C 的句子, 尾巴沒有分號; 20 但是 typedef 是 C 的句子, 所以尾巴有分號 typedef int bool; 21 且要注意 typedef 的寫法跟#define不同: #define 巨集名稱 巨集定義 22 *** 更好的方法是這範例中用的: 23 typedef {false, true } bool; 24 因為這樣以後, 不但 compiler 認識 bool, 且 false 就是 0, true是 1, 25 也就是說這樣一來, C 用起來就跟 C++ 完全一樣! 26 既然 C++ 本來就認識 bool, 因此我們用 #ifndef __cplusplus 27 使得 C 的 Compiler 要做 typedef {false, true } bool; 28 但 C++ 的 Compiler ( 認識 __cplusplus ) 則不做這 typedef! 29 這版中還沒充份利用 bool, false, true 30 =========================== **********************************/ 31 /// 若要用 Turbo C++ 的分割劃面可以參考: 32 /// ftp://ftp.csie.nctu.edu.tw/pub/CSIE/course/cs2/textwin.cpp 33 #include 34 #include 35 #include 36 37 #ifndef __cplusplus 38 //typedef int bool; // 與 #define bool int 同效果, 但要加分號; 39 typedef enum {false=0, true } bool; // 更好 :-) 跟 C++ 同樣! 40 #endif 41 42 #define MAX_PILE 7 43 #define MAX_STONE 31 44 #define MIN_STONE 5 45 46 /// =================> use Global variables 比較方便但要小心處理! 47 int nPile; /* number of Pile */ 48 int nStone[MAX_PILE + 1]; // 第 0 堆不要用, 用 1 到 7 !! 49 // 注意不用第 0 堆是因這樣跟人的習慣比較像 :-) 50 int pTake, nTake; // take from which pile, how many stones 51 52 bool lastToWin; // 1 == 拿到最後一個贏, 0 == 拿到最後一個贏輸 53 bool userFirst; // user want to Go first 54 bool gameBegin; // used in printStatus( ) 55 char msgWin[ ][33] = { "拿到最後一顆輸Lose!", "拿到最後一顆贏Win!" }; 56 57 bool gameOver( ); // indicate somebody took the last stone 58 void userTurn(void), computerTurn(void); 59 void prepareGame(void); // 把game 用到的整體(global;共用)變數準備好. 60 void playGame(void); 61 void printStatus(void); // 印出各堆石頭狀況. 62 void hello(void) { 63 printf("NIM 是從many多石頭堆中輪流拿, \r\n"); 64 // 有些compiler下不能寫 "許" ? compile時會出問題! 故寫 many 多 :-) 65 printf(" ..NIM 每次只能從一堆拿, 至少拿一個, 至多把該堆拿光,\r\n"); 66 printf(" .. 可規定拿到最後一顆贏, 或拿到最後一顆輸!\r\n\n"); 67 } 68 69 bool askYN( ){ 70 bool ans; // 1 == true == Yes, 0 == false == No 71 static char buf[333]; 72 fgets(buf, sizeof(buf), stdin); 73 if(buf[0] == 'N' || buf[0] =='n') return false; 74 if(buf[0] == 0 ) return false; // '\0' == 0 75 return true; // We like the user to play :-) So default is YES 76 } // askYN( 77 78 /// ======== ====== ====== MAIN PROGRAM ====== ====== ======== /// 79 int main( ) { 80 bool playAgain; // Local variable is enough 81 hello( ); 82 srand( time(0) &0xfffff ); // true random, see "man 3 time" 83 playAgain = true; // force to play :-) 84 while(playAgain) { 85 prepareGame( ); 86 playGame( ); 87 printf("Want to play again (Y, N)? "); 88 playAgain = askYN( ); 89 } 90 printf("\r\nThank you for your play!\nCome to play again!\n"); 91 return 0; 92 } // main 93 /////////////////////////////////////////////////////////// 94 void prepareGame(void) { 95 int i; 96 lastToWin = userFirst = (38==38); // true 97 if(rand( ) % 100 >= 50) lastToWin = false; // 50%, 就是 %2 == 1 98 nPile = 3 + rand( ) % (MAX_PILE-3+1); // 3..7 99 for(i=1; i <= nPile; ++i) { /// 注意由第一堆算起 100 nStone[i] = MIN_STONE + rand( ) % (MAX_STONE -MIN_STONE +1); 101 } 102 } // prepareGame 103 void printStatus( ){ 104 int i; 105 printf("There are 共有 %d pile堆石頭:\r\n", nPile); 106 printf(" P_%d: %d", 1, nStone[1]); // Note that 沒第 0 堆 107 for(i=2; i <= nPile; ++i) { // then pile-2, pile-3, ... 108 printf(", P_%d: %d", i, nStone[i]); 109 } 110 printf("\r\n"); 111 if(gameBegin) { // 只有剛開始才印這句 112 printf("!!! 注意 %s! %s!!\r\n", 113 msgWin[lastToWin], msgWin[lastToWin]); 114 gameBegin = false; //So that won't print again 115 } 116 } // printStatus 117 bool gameOver( ) { // Note that Pile# is 1..nPile 118 int i, totalStone=0; 119 for(i=1; i <= nPile; ++i) totalStone += nStone[i]; 120 return (totalStone <= 0); // no more stone == gameOver 121 } // gameOver 122 123 void computerTurn( ){ 124 pTake = 1; // 從第一堆看看; 注意我們一開始就規劃不用第 0 堆 125 while(nStone[pTake]==0) ++pTake; // 目前先找還有石頭的 126 nTake = 1; // 至少要拿一個 127 nTake = 1 + rand( ) % nStone[pTake]; // 隨便拿 128 // 129 // 要確保資料正確, 不可拿超過, 並想辦法能贏就要贏 130 // how to win? Hint: utilize XOR operation 131 // see wikipedia.org 132 printf(" --> I take %d from Pile %d\r\n", nTake, pTake); 133 nStone[pTake] -= nTake; 134 printStatus( ); 135 } // computerTurn 136 137 138 void userTurn( ) { 139 static char buf[88]; 140 again: // 若不用 goto, 要如何做? 可改用 while 搭配 flag 141 printf("Take from which Pile? "); 142 fgets(buf, sizeof(buf), stdin); 143 pTake = (int)atol(buf); 144 // 防呆處理: 要確保輸入資料正確, 不可讓 user 亂輸入.. 145 // ... 若該堆 0 個也不要讓他拿喔! 146 if(pTake > nPile ) { // 這版先 check pile# 太大的 147 // print 沒那麼多堆喔 ! 148 goto again; // goes back 149 } 150 again22: // 要求輸入 # of stone in that pile 151 printf(" from Pile# %d, take how many stones (1..%d)? ", 152 pTake, nStone[pTake]); 153 fgets(buf, sizeof(buf), stdin); 154 nTake = (int)atol(buf); 155 if(nTake < 0) goto again; //負數表示後悔剛輸入的 pile# 156 // 防呆處理: 要確保輸入資料正確, 不可拿超過 157 if(nTake==0 || nTake > nStone[pTake]) { 158 // print 你白痴啊? 不要亂輸入 ! 159 goto again22; // pTake OK, 但 nTake 要重新輸入 160 } //if, 思考: 若不用 這 兩層 goto, 要如何用兩層 Loop 取代??? 161 // 取得 pTake 堆 與 nTake, 阿就讓他拿掉啊 162 nStone[pTake] -= nTake; 163 printStatus( ); 164 } // userTurn 165 166 void playGame(void) { 167 int userWin; // Local variable is enough 168 gameBegin = 1; // 每次game開始要強迫印出拿最後一個會贏還是會輸 169 printStatus( ); // 印出目前各堆石頭, 若第一次印要印上列說的 170 printf(" ..Want to go first (Y, N)? "); 171 userFirst = askYN( ); 172 userWin = false; 173 if(userFirst) userTurn( ); 174 if(gameOver( ) && lastToWin) userWin = true; 175 while( !gameOver( ) ) { // take turn until no more stone 176 computerTurn( ); 177 if(gameOver( )) { 178 if(lastToWin) userWin = false; else userWin = true; 179 break; 180 } 181 userTurn( ); 182 if(gameOver( )) { 183 if(lastToWin) userWin = true; else userWin = false; 184 break; 185 } 186 } // while 187 if(userWin) { 188 printf(" Congratulations -- You Win!\r\n"); 189 }else{ 190 printf(" SOOORRRY Sorry! -- Ha Ha Ha! -- I Won!\r\n"); 191 } 192 } // playGame ** nim3.c 修改 userTurn( ) 內的 goto, 改用 while ** nim5OK.c 完整版, 把 computerTurn( ) 換成可贏就贏的策略 !