JavaでNumer0nの対戦ゲームを作ってみた（AIも作ったよ）

#JavaでNumer0nの対戦ゲームを作ってみた

##はじめに
今回、大学の授業でJavaでサーバ・クライアント間の通信を介した何かを作るという課題が出たので、高校生の頃、授業中によくやっていたNumer0nのゲームを作ってみようと思った。
高校生の頃、Numer0nのゲームにはまりすぎて、1手目で〇EAT〇BITEになる確率を手計算で求めて楽しんでた気がする．．．

この記事を読んでくださっている方は、Numer0nの基本的なルールは知っていると思うのでそこのところは割愛させていただきます。
[ヌメロンWikipedia](https://ja.wikipedia.org/wiki/Numer0n)

##開発環境
Java：version 7
Eclipse: Juno 4.2
OS: windows10


##開発方針
今回のシステムで実装したいことは、
1. サーバ・クライアント間の通信を行い、複数のクライアントが同時にアクセスでき、ルームを作成し、対戦ができる
2. それなりに強いコンピュータ対戦もできるようにする（アルゴリズムの実装）
3. 入力ミスや数値の重複などに対応する
4. ターン制のゲームなので入力を交互に受け取るようにする


##コード
コードをすべて載せると長くなってしまうので、今回はサーバ側の送受信を担当するChannelクラスと、コンピュータのアルゴリズムを実装したNumer0nAIクラスのみ掲載します。すべてのクラスと発表資料（パワポ）はGitHubに載せてあるのでよかったら見ていってください！pythonのほうでもNumeronAIを実装しているのでpythonよく使う人はそちらもどうぞ！
[Javaで作るNumer0n（GitHub）](https://github.com/SotaMatsuzawa/Numeron_Java)
[pythonのNumer0nAI（GitHub）](https://github.com/SotaMatsuzawa/Numer0n)
####Channelクラスについて
クライアントからの入力を確認するとサーバがChannelを生成し、Channelとクライアントがやり取りをします。そのおかげでクライアントに意図しない例外が発生しても、おおもとのサーバはダウンしないので他のクライアントの接続は保たれます。

```java:Channel.java
package server;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Channel extends Thread {
	JUDGE judge = new JUDGE();
	Server server;
	Socket socket = null;
	BufferedReader input;
	OutputStreamWriter output;
	String handle;
	String playertype;
	String roomnumber;
	String mynumber;
	String tekinumber;
	boolean turn;

	String ex = "926";//AIの最初の予測値
	String ca ="0123456789";//candidate number　最初は０～９まで
	String ex_number;
	List<String> old_list = new ArrayList<>();

	final char controlChar = (char)05;
	final char separateChar = (char)06;

	Channel(Socket s,Server cs){
		this.server = cs;
		this.socket = s;
		this.start();
	}

	synchronized void write(String s){
		try{
			output.write(s + "\r\n");
			output.flush();
		}catch(IOException e){
			System.out.println("Write Err");
			close();
		}
	}

	public void run(){
		List<String> s_list = new ArrayList<>();//クライアントからの入力を受け取り、ためておくリスト
		String s;
		String opponent = null;
		try{
			input = new BufferedReader(
					new InputStreamReader(socket.getInputStream()));
			output = new OutputStreamWriter(socket.getOutputStream());
			write("# Welcome to Numr0n Game");
			write("# Please input your name");
			handle = input.readLine();
			System.out.println("new user: "+ handle);
			while(true){//HOST or GUEST入力待ち
				write("INPUT YOUR TYPE(HOST or GUEST or AI)");
				playertype = input.readLine();
				if(playertype.equals("HOST")){
					Random rnd = new Random();
					s = String.valueOf(rnd.nextInt(100)+100);
					write("[HOST]ルーム番号: "+s);
					break;
				}else if(playertype.equals("GUEST")){
					write("[GUEST]ルーム番号を入力してください");
					s = input.readLine();
					write("[GUEST]ルーム番号: "+s);
					break;
				}else if(playertype.equals("AI")){
					write("[vs AIモード]");
					Random rnd = new Random();
					s = String.valueOf(rnd.nextInt(100)+100);
					write("[HOST]ルーム番号: "+s);
					break;
				}else{
					write("ルーム番号入力でエラー");
				}
			}

			roomnumber  = s;	//roomnumberの決定
			System.out.println(roomnumber);
			write("対戦相手待ち");

			if(playertype.equals("AI")){
				//AIとの対戦
				write("自分の数字を決めてください（*3桁の数値*0～9まで*数字の被りなし）");

				boolean firstnum = false;
				while(firstnum == false){//最初の自分の数字が上記の条件を満たしているか
					mynumber = input.readLine();
					firstnum = judge.isNumber(mynumber);
				}
				write("自分の数字: "+ mynumber);
				write(handle + "からスタートです");
				tekinumber="864";
				NumeronAI numeron = new NumeronAI();
				while(true){
					//ゲームスタート
					boolean finish = false;

					s = input.readLine();

					if(s == null){
						close();
					}else{
						System.out.println(s);
						boolean numsuccess = judge.isNumber(s);//数字が定義内
						if (numsuccess) {
							JUDGE eatbite = judge.EatBite(s, tekinumber);
							finish = judge.Finish(eatbite.eat);//3eatになったかどうか
							write("["+ s +"] eat: " +String.valueOf(eatbite.eat) +" bite: "+ String.valueOf(eatbite.bite));

							//ここからAIのターン
							JUDGE AIeatbite = judge.EatBite(ex, mynumber);

							NumeronAI squeeze = numeron.Squeeze(AIeatbite.eat,AIeatbite.bite,ex,ca,old_list);
							if(squeeze.new_can_list.size()<300){
								//System.out.println(Arrays.toString(squeeze.new_can_list.toArray()));
								//System.out.println(squeeze.can_num);
								ex_number = numeron.choice(squeeze.new_can_list,squeeze.can_num);
							}else{
								Random rnd = new Random();
								int index = rnd.nextInt(100);
								ex_number = squeeze.new_can_list.get(index);
							}
							old_list = new ArrayList<>(squeeze.new_can_list);
							//System.out.println("残り候補数: " + String.valueOf(old_list.size()));
							write("AIの予測値:" + ex + "		[残り候補数: " + String.valueOf(old_list.size())+"個]");
							//System.out.println("AIの予測値: "+ ex);
							if(mynumber.equals(ex)){
								write("#################you lose#################");
							}
							ex = ex_number;

								//ここまでがAIのターン
						} else {
							write(" did not send such a number");
						}
					}
					if(finish){
						write("#################you win#################");
					}
				}

			}else{//vs人間
				while(opponent == null){//対戦相手待ち
					opponent = server.findopponent(handle,roomnumber);
				}
				//write("対戦相手が決まりました");
				write("自分の数字を決めてください（*3桁の数値*0～9まで*数字の被りなし）");

				boolean firstnum = false;
				while(firstnum == false){//最初の自分の数字が上記の条件を満たしているか
					mynumber = input.readLine();
					firstnum = judge.isNumber(mynumber);
				}
				write("自分の数字: "+ mynumber);

				while(tekinumber == null){//敵の数値を取得するまで待つ
					tekinumber = server.findopponentnumber(handle, roomnumber);
				}

				if(playertype.equals("HOST")){
					turn = true;
				}else{
					turn =false;
				}

				write("HOSTプレイヤーからスタートです");
				while(true){
					//ゲームスタート
					boolean finish = false;
					while(true){
						//ターンの確認
						s_list.add(input.readLine());//入力を入れておく
						turn = server.isTurn(handle);//turnの確認
						if(turn == true){
							break;
						}
					}

					s = s_list.get(s_list.size()-1);
					s_list.clear();

					if(s == null){
						close();
					}else{
						System.out.println(s);
						boolean numsuccess = judge.isNumber(s);//数字が定義内
						if (numsuccess) {
							//write("judge ok");
							boolean connectsuccess = server.singleSend(opponent,"[相手の予測] "+s);//相手がいる
							if(connectsuccess){
								//write("相手が存在する");
								JUDGE eatbite = judge.EatBite(s, tekinumber);
								finish = judge.Finish(eatbite.eat);//3eatになったかどうか
								write("		[自分の予測]"+ s +" eat: " +String.valueOf(eatbite.eat) +" bite: "+ String.valueOf(eatbite.bite));
								server.ChangeTurn(handle, opponent);//ターンの切り替え
							}else{
								write("did not find opponent");
							}
						} else {
							write(" did not send such a number");
						}


						}
					if(finish){
						write("#################you win#################");
						server.singleSend(opponent, "#################you lose#################");
					}

					}
			}



		}catch(IOException e){
			System.out.println("Exception occurs in Channel: "+handle);
		}
	}
	public void close(){
		try{
			input.close();
			output.close();
			socket.close();
			socket = null;
			//server.broadcast("回線切断 : " + handle);
		}catch(IOException e){
			System.out.println("Close Err");
		}
	}



}

```


####NumeronAIクラスについて

NumeronAIクラスはコンピュータの予測部分を実装したクラス。
予測には、得られたEAT-BITE情報から考えられる候補を絞るSqueezeメソッドと、その候補の中から、良さそうな手を選ぶChoiceメソッド、良さそうな手を計算するcount_candメソッドからなります。良さそうな手とは、ある手を選んだ時に、返ってくるすべてのEAT-BITEの組み合わせから期待正解候補数が最も少ないものとしています。詳しくは[numer0nの必勝法を考える](http://cygx.mydns.jp/blog/?arti=457)を見てみてください。この理論に近いものを実装してます！
ちなみに平均コール数は5か6ぐらいだと思います（体感）。


```java:NumeronAI.java
package server;

import java.util.ArrayList;
import java.util.List;

public class NumeronAI {
	List<String> new_can_list = new ArrayList<>();//candidate list
	String can_num;//candidate number


	//考えられる候補数に絞る
	public NumeronAI Squeeze(int eat,int bite,String pred_num,String ca_num,List<String> old_list){
		NumeronAI squeeze = new NumeronAI();
		List<String> can_list = new ArrayList<>();
		List<String> li = new ArrayList<>();

		if(eat == 0 && bite == 0){
			//System.out.println("--------" + String.valueOf(ca_num.length()));
			for(int i = 0; i<ca_num.length();i++){
				if(ca_num.charAt(i) != pred_num.charAt(0) && ca_num.charAt(i) != pred_num.charAt(1) && ca_num.charAt(i) != pred_num.charAt(2)){
					li.add(String.valueOf(ca_num.charAt(i)));
				}
			}
			ca_num ="";
			StringBuilder builder = new StringBuilder();
			for(String num : li){
				builder.append(num);
			}
			ca_num = builder.substring(0,builder.length());

			for(int i = 0;i<ca_num.length();i++){
				for(int j = 0;j<ca_num.length();j++){
					for(int k = 0;k<ca_num.length();k++){
						if(ca_num.charAt(i)!=ca_num.charAt(j) && ca_num.charAt(i)!=ca_num.charAt(k) && ca_num.charAt(j)!=ca_num.charAt(k)){
							can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j))+String.valueOf(ca_num.charAt(k)));
						}
					}
				}
			}
		}else if(eat ==0 && bite ==1){
			for(int i = 0;i<ca_num.length();i++){
				for(int j = 0;j<ca_num.length();j++){
					if(ca_num.charAt(i) != ca_num.charAt(j) && ca_num.charAt(i)!=pred_num.charAt(0) && ca_num.charAt(i)!=pred_num.charAt(1) && ca_num.charAt(i)!=pred_num.charAt(2)){
						can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(j)));
						can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j))+String.valueOf(pred_num.charAt(0)));
						can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j)));
						can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j))+String.valueOf(pred_num.charAt(1)));
						can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j)));
						can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(j)));
					}

				}
			}
		}else if(eat ==0 && bite ==2){
			for(int i = 0;i<ca_num.length();i++){
				if(ca_num.charAt(i)!=pred_num.charAt(0) && ca_num.charAt(i)!=pred_num.charAt(1) && ca_num.charAt(i)!=pred_num.charAt(2)){
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(1)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(0)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(1)));
					can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(i)));
					can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(0)));
					can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(i)));
					can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(0)));
					can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(1)));
					can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(i)));
				}
			}
		}else if(eat == 0 && bite ==3){
			can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(0)));
			can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(1)));

		}else if(eat == 1 && bite ==0){
			for(int i = 0;i<ca_num.length();i++){
				for(int j = 0;j<ca_num.length();j++){
					if(ca_num.charAt(i)!=ca_num.charAt(j) && ca_num.charAt(i)!=pred_num.charAt(0) && ca_num.charAt(i)!=pred_num.charAt(1) &&
							ca_num.charAt(i)!=pred_num.charAt(2) && ca_num.charAt(j)!=pred_num.charAt(0) &&
							ca_num.charAt(j)!=pred_num.charAt(1) && ca_num.charAt(j)!=pred_num.charAt(2)){
					can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(j)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(ca_num.charAt(j))+String.valueOf(pred_num.charAt(2)));
					}
				}
			}

		}else if(eat ==1 && bite ==1){
			for(int i = 0;i<ca_num.length();i++){
				if(ca_num.charAt(i)!=pred_num.charAt(0) && ca_num.charAt(i)!=pred_num.charAt(1) && ca_num.charAt(i)!=pred_num.charAt(2)){
					can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(1)));
					can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(2))+String.valueOf(ca_num.charAt(i)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(0)));
					can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(i)));
					can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(2)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(2)));
				}
			}

		}else if(eat ==1 && bite ==2){
			for(int i = 0;i<ca_num.length();i++){
				can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(1)));
				can_list.add(String.valueOf(pred_num.charAt(2))+String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(0)));
				can_list.add(String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(2)));
			}

		}else if(eat ==2 && bite ==0){
			for(int i = 0;i<ca_num.length();i++){
				if(ca_num.charAt(i)!=pred_num.charAt(0) && ca_num.charAt(i)!=pred_num.charAt(1) && ca_num.charAt(i)!=pred_num.charAt(2)){
					can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(pred_num.charAt(1))+String.valueOf(ca_num.charAt(i)));
					can_list.add(String.valueOf(pred_num.charAt(0))+String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(2)));
					can_list.add(String.valueOf(ca_num.charAt(i))+String.valueOf(pred_num.charAt(1))+String.valueOf(pred_num.charAt(2)));
				}
			}

		}else if(eat ==3 && bite ==0){
			can_list.add(pred_num);
		}

		if(old_list.size()!=0){
			for(String num : can_list){
				if(old_list.contains(num)){
					squeeze.new_can_list.add(num);
					squeeze.can_num =ca_num;
				}
			}

		}else{
			squeeze.new_can_list = can_list;
			squeeze.can_num = ca_num;
		}
		//System.out.println(can_num);
		return squeeze;
	}


	//期待候補数を計算する
	public double count_cand(String pred_num,String ca_num,List<String> ca_list){
		double ave_ca = 0;
		int[][] info_list = {{0,0},{0,1},{0,2},{0,3},{1,0},{1,1},{1,2},{2,1},{3,0}};
		int sum_ex = 0;
		int sum_ex2 = 0;
		List<String> old_count_list = new ArrayList<>(ca_list);
		String ca_count_num = ca_num;
		NumeronAI squeeze2 = new NumeronAI();
		for(int[] info :info_list){
			squeeze2 = Squeeze(info[0],info[1],pred_num,ca_count_num,old_count_list);
			sum_ex=sum_ex+squeeze2.new_can_list.size();
			sum_ex2=sum_ex2+squeeze2.new_can_list.size()^2;
		}
		if(sum_ex!=0){
			ave_ca=sum_ex2/sum_ex;
		}
		return ave_ca;
	}


	//期待候補数が最小の数値を選択する
	public String choice(List<String> ca_list,String ca_num){
		List<Double> ave_list = new ArrayList<>();
		int min_index =0;
		try{
			for(String num :ca_list){
				double ave_ca = count_cand(num,ca_num,ca_list);
				ave_list.add(ave_ca);
			}
			double min =ave_list.get(0);
			for(int i =0;i<ave_list.size();i++){
				double val = ave_list.get(i);
				if(min > val){
					min = val;
					min_index = i;
				}
			}
			return ca_list.get(min_index);
		}catch(Exception e){
		System.out.println("チョイスミス:" + e);
		return "111";
	}
	}


}

```


##実行結果
![実行結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436885/302dc9cc-524e-6678-7f83-4972548c9bad.png)
今回だと、6ターンで当てられてしまいました。

##感想
Javaはあまり書いたことがなかったけど一応自分の作りたかったものはできたと思います。
しかしまだまだ、デザインパターンの知識を実践するレベルには至ってないので、勉強する必要があるなぁと実感しました。（デザインパターン難しい．．．）
Numer0nのAI(AIとあまり言いたくないけど)を自分の手で書いてみるのは、Numer0nを知っていて、なにか作ってみたいという方には、ルールも分かりやすいのでオススメです！

あと、pythonでもNumeronAIの部分だけ作成したのでJavaは書いてないけどpythonならわかるよって方は良かったら参考にしてみてください。

Qiitaの投稿はこれが初めてなので、Qiitaの先輩方、この記事について改善点などありましたら是非コメントよろしくお願いします<(_ _)>

これから少しずつアウトプットしていけるよう頑張ります！