//CowBull.java  -- 可搭配 NetBull.java, 也可自己 Run !
/*** modified from CowBull.c   -- 電腦猜人想的 Bull & Cow game
 * 請參看  http://en.wikipedia.org/wiki/Bulls_and_cows
 * Also try here   http://pyva.net/cgi-bin/bk
 *                 http://www.funmin.com/online-games/bulls-and-cows/
 *       http://bullscows.com/   (注意它給的不是我們說的 幾 A 幾 B 喔 )
 *  本程式可以獨立編譯與執行, 也可以搭配 NetBull.java 成為網路版 
 *  因為這程式用 reflection 的功能使得編譯時不需有 Brother.class
 *          @CopyLeft by tsaiwn@csie.nctu.edu.tw
 ***********=====================================******************/
import java.io.*;
import java.util.*;   // StringTokenizer in this package
import java.lang.reflect.*;  // reflection, 以便不直接用到 Brother.class
               //reflect 是指 RUN time 可查知物件型別資訊甚至生出物件...
class CowBull {
     BufferedReader cin=null;  //我們要用 BufferedReader 的 readLine( )
     PrintStream cout=System.out, cerr=System.err;
     Object sys=null;  // who is running me, if use with NetBull.java
     String name = " ";   // 學號姓名, if with NetBull.java
     String record = " ";  // 紀錄猜的過程備查  
     public static final int BOTH_AB = 3000;  // 若同時回答兩答案 ?A ?B 
  ///
     int possible[ ] = new int[5040];    // global variable
     int npsible;    // global variable 記著有幾個可能答案 //
   public static void main(String xx[ ]) {
        new CowBull( );
   } // main for single player

   CowBull( ) {    // 單機版的 constructor, 寫個 main 然後 new 自己就可 
      prepareIO( );   // 把鍵盤和螢幕 stream 準備好, 稱 cin, cout 
      play( );
   }
   void prepareIO( ) {
      cin = new BufferedReader( 
               new InputStreamReader( System.in ) );
      cout = new PrintStream(System.out);  //PrintStream 才有print( )
   }
 // 以下的 constructor 是讓  NetBull.java 搭配使用, him 是 Brother 類別 
 CowBull(BufferedReader cin, PrintStream cout, Object him) {
   this.cin = cin;   // 因與參數同名, 須用 this.cin 才是這 class 的 cin
   this.cout = cout;
   this.sys = him;   // 指回去正在 run 我的 Brother, 用它的name, log( ) 
   this.name = getMyName( );  // try from Brother class if any
   play( );
 }
// C 程式中原來的 main, 大部分都沒改
int play( ) {
       int n, tmp, nGuess, nBull, nCow;
       int yn, myGuess;
       //long now= time(0);
       //srand( now &0xffff);    /* randomize, see "man srand" */
      record= " ** " + name + ": ";
      boolean quit = false;    // 不離開, 要繼續玩 :-) 
    while( ! quit ) {
    //again:
       npsible = prepareAnswers( );   // 放在 possible[5040]
       nBull = nCow = nGuess = 0;
       while(nBull != 4) {
          // 由所有可能答案中隨便挑一個當 myGuess, 並印出說猜該數 //
           n = rand( ) % npsible;    /* 0.. npsible -1 */
           tmp=possible[0]; possible[0]=possible[n]; possible[n]=tmp;
           myGuess = possible[0];   /* 永遠猜第 0 個比較好寫 */
           cout.printf( "I guess your number" +
                           " is %04d, ... ", myGuess);
           ++nGuess;  // 猜的次數
           /// /// 由 user 取得幾A放 nBull, 幾 B 放 nCow
           cout.print("\r\n \t\t  幾 A  幾  B? ");
           //getAnswer(&nBull, &nCow);  //nBull 就是幾 A
           nBull = getAB( );  // 問幾A幾B, 但user 可能只回答幾 A
           // 若 nBull 很大, 表示含有 nCow 值: nBull*10+nCow + BOTH_AB
           if(nBull >= BOTH_AB) {  // he keyin 2 answers
               nBull = nBull - BOTH_AB;   // BOTH_AB 是前面寫的常數 
               nCow = nBull %10;
               nBull = nBull / 10;   // 注意 整數除以整數是整數 !
           }else if(nBull != 4) {   // 只回答 幾 A, 且不是 4 A
                // 再問一次, 取得幾  B ?
             cout.print("\r \t\t  " + nBull + " A 以及 幾  B? ");
             nCow = getInt( ) % 10;  // 只取個位數 
           }
           putRecord(myGuess, nBull, nCow); // proof
           if(nBull == 4){
               if(nGuess<5)cout.printf( "才 %d 次", nGuess);
               cout.printf("喔 Yeah 我猜到了!\n");
               if(nGuess>6)cout.printf("這麼衰..猜了 %d 次!\n", nGuess);
               log(3, " -->got "+myGuess+" in "+nGuess+" times "
                       + " with " + name);
               break;
           }
           cout.printf("%04d : %d A %d B; ", myGuess, nBull, nCow); //
           /* 還沒猜對, 把不可能的答案刪除並修正 npsible */
           npsible = reduceAnswers(myGuess, nBull, nCow);
           if(npsible <= 0){
               cout.printf( "You cheating 欺騙我!\n");
               log(5, " -->??? "+myGuess+ " in " + nGuess + " times "
                       + " cheating by " + name);
               log(5, "    ??? " + record); // proof of his cheating
               cout.printf("\r You see 看: " + record);
               break;
           }
       } // while 還沒猜到 4 A; 注意幾A(公牛)指數字對且位置也對!
       cout.print("\r\n\t 要不要繼續玩下一攤(yes, no)? ");
       record= " " + name + ": ";   // reset record
       yn = askPlayAgain( );   // 1 表示 yes, 0 為 no 
       if(yn == 0) quit = true;  //goto again;
    }//while(!quit
       cout.println( "Thank you and Bye! " + new Date( ) );
    return 0;
} // play(
//function to check if the user want to play again
int askPlayAgain(){    /* return 1 if he/she said YES */
    String tmp="no";  //static char tmp[99];
    try {
       tmp = cin.readLine( );  // readLine(tmp);
       if(tmp==null) tmp="no";  // 防呆, EOF (CTRL_Z)會 null
       tmp = edit(tmp);
    }catch(Exception e) { }
    if(tmp.length( ) == 0) return 1;  // 只按 ENTER
    if(tmp.charAt(0) == 'Y' || tmp.charAt(0) == 'y') return 1;
    if(tmp.charAt(0) == 'N' || tmp.charAt(0) == 'n') return 0;
    cout.printf( "Please type N or Y: ");
    return askPlayAgain();    /* recursively call myself */
}
/// function   prepareAnswers();
int
prepareAnswers(){
      //想辦法 (用四層 for loop ) 把所有可能答案 放在 possible[5040]
      //共有 5040 個, 最小 0123, 最大 9876
      //(因每位不能重複)
     //int * x = possible;    /* 之後 x[0] 就是 possible[0] */
    // 以下是原來給大家的例子
    int p=0, i,k,m,n;
    for(i=0; i<=9; i++){
        for(k=0; k<=9; k++){
            if(k==i) continue;   /* 第二位k與第一位數i相同的都不要 */
            for(m=0; m<=9; m++){
                if(m==k || m==i) continue;
                for(n=0; n<=9; n++){
                   if(n==m || n==k || n==i) continue;
                   possible[p++] = i*1000+k*100+m*10+n;
                }
            }//for m
        }//for k
    }//for i
    return p;
}
/// function reduceAnswers()
int reduceAnswers(int myGuess, int nBull, int nCow) {
       // 刪除不可能之答案, 並將可能之答案放在 array 前端 //
       // 以下只做刪除不可能之答案 //
       /// 順便把可能答案往 array 前端移 : 留給大家想 ..
       int na=0, nb=0, i;
       int n= 0;  /* possible[0] 是第一個可以蓋掉的 */
             /* 所以由 possible[1] 開始看看有哪些是有可能的就留下來 */
       for(i=1; i< npsible; i++){
             //叫用以前的 checkAnswer(要稍微修正) 比較 myGuess 和 possible[i]
             //以便知道myGuess對於 possible[i]是幾A幾B //
             //若幾A幾B剛好與 nBull 和 nCow 完全相同表示為可能答案 //
             //否則都要消去 //

            int kkk = checkAnswer(myGuess, possible[i], na, nb, i);
            na = kkk/10;  nb = kkk%10;
            if(na == nBull && nb==nCow){
                possible[n] = possible[i];    /*可能的答案移到 array 較前面*/
                ++n;   /* n 指向下一個可蓋掉的元素 possible[n] */
            }
       }//for i
       npsible = n;
       return n;
}
/****** 要稍微修正checkAnswer, 因以前是比兩個 array,
               現在是比兩個整數, 要先用 %10 的方法拆成四位數 ! //
 *************************************************************/
 int  checkAnswer(int xx, int yy, int na, int nb, int item){
    int x[ ]= new int[4], y[ ] = new int[4], i, j;
    na = nb =0;
    /* 要先把整數拆解成四位個位數 */
    x[0] = xx % 10; y[0] = yy % 10;
    x[1] = (xx %100) /10; y[1] = (yy %100) /10;
    x[2] = (xx /100) %10; y[2] = (yy /100) %10;
    x[3] = xx /1000; y[3] = yy /1000;
    for(i=0; i<=3; i++){
        if(x[i] == y[i]) ++(na);
        for(j=0; j<=3; j++)
           if(i!=j && x[i] == y[j]) ++(nb);
    }//for i
    return na*10 + nb;
 }
 int getInt( ) {
    String s = "000";
    try {
       s = cin.readLine( );
       s = edit(s);
       if(s.length( ) == 0) s = "0"; 
    }catch(Exception e) {  }
    int ans =0;
    //if(s.length( ) == 0) return 0;
    try {
       ans = Integer.parseInt(s);
    }catch(Exception e) { ans = 0; }
    return ans;

 }
 int getAB( ) {  // 問 player 幾 A 幾 B? 若回答 二者, + BOTH_AB 做區別 
    int na=0, nb=0;
    String s = "0";
    try {
       s = cin.readLine( );
       s=edit(s);  // 處理 Backspace 
       if(s.length( ) == 0 ) s= "0";  // 只有按 ENTER 當作 0
    }catch(Exception e) { s="0"; }
    StringTokenizer stk = new StringTokenizer(s, " ,\t"); 
    if( stk.hasMoreTokens( ) ) {   //StringTokenizer in java.util.*
        try {
          na = Integer.parseInt( stk.nextToken( ) ) % 10; // < 10
        }catch(Exception e) { na = 0; }  // 若亂則打當作 0
    }
    if( stk.hasMoreTokens( ) ) {  // nb 母牛數 
        try {
          nb = Integer.parseInt( stk.nextToken( ) );
        }catch(Exception e) { nb = 0; }
        na = na*10 + (nb%10) + BOTH_AB; // indicate 2 answers
    } // 
    return na;   // 若 >= BOTH_AB 表示有兩個答案 ?A ? B
 }//getAB(
 /// ///VVVVVVVVVVVV
 /// 模仿 C 語言中的 Library function rand( )
 int rand( ) { return (int) (Math.random( ) * 32767 ); }

 /// /// 因為打入 Backspace 其實留在輸入字串中, 須自己處理 
 String edit(String s) {   // 去掉 s 中的 Backspace 以及前後空白 
    String t = s.trim( );  // 去掉頭尾空白 
    char u[ ] = t.toCharArray( );   // 轉為 char array [ ]
    int p = 0;
    int last = u.length;
    for(int i=0; i< last; ++i) { // 127 為 DEL key 一般不處理 
       //System.out.print(" :" + (int)u[i]);
       if((int)u[i] == 127){  continue; }  // DEL key
       if((int)u[i] == 8) { if(p>=0) --p; if(p>=0) --p;} //Backspace
       else u[p] = u[i];   // 把有用的字 copy 到左邊尾巴 
       ++p; 
    }
    while(p<last) u[p++] = ' ';  // 後面都清掉 
    t = new String(u);  // 轉回 String
    return t.trim( );   // 再去掉頭尾空白 
 } //edit(

 // 以下 putRecord( ) 用來把每次猜的過程 append 到 record
 void putRecord(int guess, int na, int nb) {
    String ss;
    if(guess <= 999) ss = "0" + guess;  // 若是三位數, 左邊要補 0 
    else ss = ""+guess;   //  if(ss.length( ) < 4) ss = "0" + ss;  
    record += " "+ ss +"("+ na+"A " +nb+"B), ";  // record 是 Global 
 }

 /// ///VVVVVVVVVVVVVV
 void log(int k, String s) {  // 傳給我的老大哥去 Log 就可 
    if(sys == null) return;   // 若 sys 不是 null 則為網路版 
    //sys.log(k, s);   // good if Brother sys;  //但現在 sys 是 Object
    Method doLog=null;       // 這樣寫在 javac 時就不用有 Brother.class
    try {
       Class c = sys.getClass( );  // 取得 class name ( Brother )
       Class[ ] par={Integer.TYPE, String.class}; // log(int, String);
       doLog = c.getDeclaredMethod("log", par);  // get取得長這樣的 log( )
       Object[ ] ppp= new Object[2];
       ppp[0] = new Integer(k);  // prepare to call log(k, s); in Brother
       ppp[1] = s;    // String 本來就是  Object
       doLog.invoke(sys, ppp);  // 叫 sys.log(k, s);
    }catch(Exception e) { return; }  // 若失敗就算了 :-)
 }

 /// ///VVVVVVVVVVVVVV 取得 NetBull.java 中 Brother class 內的 name
 String getMyName( ) {     // 這樣寫在 javac 時就不用有 Brother.class
    if(sys == null) return "me";
    try {      // our purpose is only to get sys.name in Brother.class 
       Class c = sys.getClass( ); // sys 是 Brother 傳來的 
       Field f = c.getDeclaredField("name");  // 抓到 name 的參考 
       return f.get(sys).toString( );    // 取得 sys.name 的值 
    }catch(Exception e) { return "???"; }  //若有錯就傳回 "???"
 }// getMyName
} // class
