JavaでNumer0nみたいなゲームを作ってみた

この記事は [リンク情報システム](https://www.lis.co.jp/) の「[2019 Tech Connect Summer](https://qiita.com/dgkz/items/2862d5e6b77203a126b3)」のリレー記事です。
engineer.hanzomon のグループメンバーによってリレーされます。

(リンク情報システムのFacebookは[こちらから](https://www.facebook.com/lis.co.jp/))

うさちゃんチームの8日目は@k-igarashi214と@k-nakamura0420の2人でお送りします！

#「Numer0n」って？

「Numer0n」は、何年か前にフジテレビで放送されていたゲームバラエティ番組です。

EATやBITEの数から相手のナンバーを推理するあれです。（詳細は[Wikipedia](https://ja.wikipedia.org/wiki/Numer0n)を参照）

#作るもの

今回はJavaの勉強として、「**Numeran**」という名前でコンソールで動くゲームを作っていきます。

この「Numeran」ですが、本家「Numer0n」からルールを１つだけ変更しました。

それは、**「自分のナンバーを決める際、同じ数字を被せてもよい」**です。
（例：「2234」、「0101」、「5555」など）
~~決して数字の重複チェックが面倒だったからではありません~~

これを踏まえてEATとBITEの求め方を例を挙げて説明します。

####　EATとBITEの求め方
- 例1　相手のナンバーが「1234」の場合

「1430」をコール
1桁目の「1」　⇒　数字も桁も一致　　　　⇒　EAT
2桁目の「4」　⇒　数字はあるが桁が違う　⇒　BITE
3桁目の「3」　⇒　数字も桁も一致　　　　⇒　EAT
4桁目の「0」　⇒　数字なし
⇒　2EAT - 1BITE


- 例2　相手のナンバーが「0123」の場合

「1212」をコール
1桁目の「1」　⇒　数字はあるが桁が違う　⇒　BITE
2桁目の「2」　⇒　数字はあるが桁が違う　⇒　BITE
3桁目の「1」　⇒　数字はあるが桁が違う　⇒　BITE
4桁目の「2」　⇒　数字はあるが桁が違う　⇒　BITE
⇒　0EAT - 4BITE

見てもらえればわかる通り、必ず **EAT数+BITE数 ≦ 桁数** となります。
4桁の場合は4EATで勝利です。

#ソースコード
##メインクラス
```Numeran.java
/** Mainクラス */
public class Numeran {

	public static void main(String[] args) throws IOException {

		// プレイヤーを生成
		Player atk = new Player("Player1");
		Player def = new Player("Player2");

		// タイトル表示
		System.out.println("\n----- Let's Numeran!! -----\n");

		// 先攻のNumberを設定
		System.out.print(atk.getName()+" : "+DIGIT+"桁のNumberを入力 >");
		atk.getNumber().setNumber(numInput());
		System.out.print(NEW_LINE);

		// 後攻のNumberを設定
		System.out.print(def.getName() + " : "+DIGIT+"桁のNumberを入力 >");
		def.getNumber().setNumber(numInput());
		System.out.print(NEW_LINE);

		while(true) {

			// 相手のNumberをコール
			System.out.print(atk.getName()+" : 相手のNumberを入力 >");
			int[] key = numInput();

			int eat  = def.getNumber().getEAT(key);
			int bite = def.getNumber().getBITE(key);

			// EAT数とBITE数を表示
			System.out.println("\n [ "+eat+" EAT  -  "+bite+" BITE ]\n");

			// 全ての桁がEAT
			if(eat == DIGIT) break;

			// 攻守入れ替え
			Player swap = atk;
			atk = def;
			def = swap;
		}

		// リザルト表示
		System.out.println("----- "+atk.getName()+" WIN!! -----");
	}
}
```

###解説

 `atk.getNumber().setNumber(numInput()); `
 numInput()でキーボードからナンバーを取得しています。

取得したナンバーは、Playerクラスの中のNumberクラスの中のフィールドに代入しています。
オブジェクト指向のカプセル化でフィールドを直接参照できなくしたため、getメソッドやsetメソッドを使っています。
  
##入力チェッククラス
```Input.java
/** 入力チェッククラス */
public class Input {

	// 入力チェック  戻り値 Number(int[]型)
	public static int[] numInput() throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		String numberStr; // 入力文字列格納用

		// キーボード入力,入力形式チェック
		while(!Pattern.matches("^[0-9]{"+DIGIT+"}$", numberStr = br.readLine())) {
			System.out.print("正しい形式で入力してください >");
		}

		// 入力された文字列をint[]型に変換
		return Arrays.stream(numberStr.split("")).mapToInt(Integer::parseInt).toArray();
	}
}
```
###解説

`while(!Pattern.matches("^[0-9]{"+DIGIT+"}$", numberStr = br.readLine()))`
Pattern.matchesは第一引数で指定した正規表現に第二引数の文字列が一致しているかを調べるメソッドです。この場合は入力された文字列が半角数字DIGIT桁のときにTrue、それ以外にFalseを返します。

半角数字DIGIT桁以外が入力されたときに入力エラーとして再度入力させたいので、whileの条件式に！がついています。

つまりこの数行で入力と入力チェックができます。<font color="#ccc">今までif文を大量に書いてチェックしてたのはなんだったのか・・</font>
 
`Arrays.stream(numberStr.split("")).mapToInt(Integer::parseInt).toArray(); `
入力された文字列をint[]型に変換しています。（例："1234" → {1,2,3,4} ）

実装にはStream APIを使っています。やっていることは以下の通りです。
1. numberStr.spilit("")で文字列(numberStr)を1文字ずつ分割してString[]型に格納
2. Array.stream()で1で生成したString[]型を元にStreamインスタンスを生成
3. mapToInt(Integer::parseInt)で全ての要素をint型に変換
4. toArray()でint[]型に変換

##Playerクラス
```Player.java
/** プレイヤー情報 */
public class Player {

	private String name; // プレイヤー名
	private Number number; // Number情報

	private Player() {}

	// ゲームの準備
	public Player(String name) {
		this.name = name;
		this.number = new Number();
	}

	// プレイヤー名を取得
	public String getName() { return name; }

	// Number情報を取得
	public Number getNumber() { return number; }

}
```

###解説

`private Player() {} `
引数なしのコンストラクタの修飾子をprivateにしてインスタンスの生成を制限しました。
代わりに引数ありのコンストラクタを定義することで、プレイヤー名の決定とNumberクラスのインスタンス生成を強制しています。

##Numberクラス
```Number.java
import com.google.common.primitives.Ints;

/** Number情報 */
public class Number {

	private int[] number; // Number

	// Numberを保存
	public void setNumber(int[] number) { this.number = number; }

	// EAT数を取得
	public int getEAT(int[] number) {
		return (int)IntStream.range(0,DIGIT)
							 .filter(i -> number[i] == this.number[i])
							 .count();
	}

	// BITE数を取得
	public int getBITE(int[] number) {
		return (int)IntStream.range(0,DIGIT)
							 .filter(i -> number[i] != this.number[i])
							 .filter(i -> Ints.asList(this.number).contains(number[i]))
							 .count();
	}

}
```

###解説


 `(int)IntStream.range(0,DIGIT)
　　　　　　　　　.filter(i -> number[i] == this.number[i])
　　　　　　　　　.count(); `
this.numberに対してnumberが何EATなのか求めています。

ここでもStream APIを使っています。やっていることは以下の通りです。
1. IntStream.range(0,DIGIT)で0からDIGIT-1の要素を持つIntStreamインスタンスを生成
2. filter(i -> 条件)で、条件を満たす要素のみのIntStreamを返す（iには0からDIGIT-1が入る）
3. count()でIntStreamの要素数を返す（long型なので最後にint型にキャスト）

つまり、number[i] == this.number[i]を満たす要素がいくつあるかカウントしています。
<br>
`(int)IntStream.range(0,DIGIT)
　　　　　　　　　.filter(i -> number[i] != this.number[i])
　　　　　　　　　.filter(i -> Ints.asList(this.number).contains(number[i]))
　　　　　　　　　.count();`
this.numberに対してnumberが何BITEなのか求めています。

ここでも(ry

EATと異なる点はfilterの数と条件式です。
1つ目のfilterで、EATとなる要素をIntStreamから排除します。
2つ目のfilterで、this.numberにnumber[i]が含まれているかを判定し、含まれていなければ同様にIntStreamから排除します。

※Ints.asListはint[]型をList&lt;Integer&gt;に変換するメソッドです。外部jarファイルをインポートする必要があります。

##定数クラス
```Constants.java
/** 定数クラス */
public class Constants {

	static final int DIGIT = 4; // 桁数

	// 連続改行用文字列
	static final String NEW_LINE = String.join("", Collections.nCopies(99,"\n"));
}
```

###解説

`String.join("", Collections.nCopies(99,"\n"))`
改行を99回繰り返した文字列を生成しています。
自分のナンバーを設定した後にそれを画面上に表示させないために使います。

#遊んでみた
実際に@k-nakamura0420と@k-igarashi214で遊んでみました！
@k-nakamura0420がPlayer1、@k-igarashi214がPlayer2です。
~~負けた方が勝った方にアイスを奢ります！！~~

###タイトル画面とナンバー設定

```
----- Let's Numeran!! -----

Player1 : 4桁のNumberを入力 >0224
```
@k-nakamura0420が設定したナンバーは**0224**
入力された後に大量に改行されるので、画面上から見えなくなります。

```
Player2 : 4桁のNumberを入力 >0214
```
@k-igarashi214が設定したナンバーは**0214** <font color="#ccc">ユーザー名の数字ですはい</font>
選んだナンバーがほとんど一緒:joy:

###ゲームスタート！

```
Player1 : 相手のNumberを入力 >9876

 [ 0 EAT  -  0 BITE ]

Player2 : 相手のNumberを入力 >0001

 [ 1 EAT  -  2 BITE ]

Player1 : 相手のNumberを入力 >0023

 [ 1 EAT  -  2 BITE ]

Player2 : 相手のNumberを入力 >4555

 [ 0 EAT  -  1 BITE ]
```
初手0EAT-0BITEは強いですね:sunglasses:（この時点で6,7,8,9が使われていないことが確定しました）
というか数字被りありのルールってどう攻めていったらいいんでしょう・・・

###結果は・・・
```
Player1 : 相手のNumberを入力 >0103

 [ 1 EAT  -  2 BITE ]

Player2 : 相手のNumberを入力 >0426

 [ 2 EAT  -  1 BITE ]

Player1 : 相手のNumberを入力 >0324

 [ 2 EAT  -  1 BITE ]

Player2 : 相手のNumberを入力 >0224

 [ 4 EAT  -  0 BITE ]

----- Player2 WIN!! -----
```
@k-igarashi214の勝利！！
6手で決着がつきました。

@k-nakamura0420君、
なんで負けたか、明日までに考えといてください:grin:

#感想
@k-igarashi214
割と短く書けたので満足:slight_smile:
オブジェクト指向は勿論、正規表現やStream APIも盛り込めたのでJavaの良い勉強になりました。

中でも**Stream API**は初めて使ってみましたが、とっても便利。
元々forやifでネストしていたEATやBITEの計算が、これを使って1行で済ませられました:v:
<font color="#ccc">（結局見やすくするために改行してますが）</font>

@k-nakamura0420
やってて楽しかった！やっぱり勉強したことはこうして自分が楽しめる形でアウトプットしていくべきだと思う。
あとは本家「Nume0n」にはアイテムなんていう別の要素もあったりするので、次はそっちも実装してみたい:relaxed:
負けた理由は明日までに考えておきます:sweat_smile:

---

最後までご覧いただきありがとうございました:pray:
「[2019 Tech Connect Summer](https://qiita.com/dgkz/items/2862d5e6b77203a126b3)」の他の記事もぜひご覧ください！

次回、9日目のうさちゃんチームの担当は@yoshi-hanzoさんです。お楽しみに！

[リンク情報システム株式会社](https://www.lis.co.jp/)では一緒に働く仲間を[随時募集](https://www.lis.co.jp/employ_top)しています。また、お仕事のご依頼、ビジネスパートナー様も募集しております。[お気軽にご連絡](https://www.lis.co.jp/inquiry)ください。