Java(コンプゲームを作成しながらコレクションの理解を深める)

Java
[お題]
サイコロを1回ふるのに300円かかる。
1~6の目をコンプリートできると賞金4000円もらえる。

さて、あなたはこのゲームに挑戦するであろうか?

うまくいけば6回(1800円)でコンプリートできる。しかし、それは相当難しそうだ。
13回(3900円)までにすべて揃えば得をできるわけだがどうだろう?このゲームはやるべきなのだろうか・・・。
今回はこのお題をプログラミングを使って考えてみよう。

実際に500回シミュレーションを行い、コンプするまでに要した平均額とコンプするにのに要した回数のうち一番よく現れた回数(最頻値)を求めていこう。

実行例

***************************結果*****************************
1( 300):
2( 600):
3( 900):
4( 1200):
5( 1500):
6( 1800):*******
7( 2100):*******************
8( 2400):*****************************
9( 2700):*****************************************
10( 3000):*******************************
11( 3300):************************************************
12( 3600):*************************************
13( 3900):***********************************
14( 4200):*******************************************
15( 4500):*********************************
16( 4800):****************************************
17( 5100):****************************
18( 5400):******************
19( 5700):***************
20( 6000):******************
21( 6300):*********
22( 6600):******
23( 6900):*******
24( 7200):*****
25( 7500):******
26( 7800):*********
27( 8100):****
28( 8400):***
29( 8700):
30( 9000):****
31( 9300):
32( 9600):*
33( 9900):
34(10200):*
35(10500):
36(10800):*
37(11100):*
38(11400):
39(11700):
40(12000):
41(12300):
42(12600):
43(12900):
44(13200):
45(13500):*
コンプ平均値:4306円
モード(最頻値):11回(3300円)

作成

メソッド

まずはいつものようにメソッドを作っていこう。

static int diceCompCount()

まずは何回でコンプできたかをシミュレーションできるメソッドがあると便利そうだ。

	static int diceCompCount(){
		Random rand=new Random();
		Set<Integer> set=new HashSet<>();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}

1~6までの6種類コンプするまでに何回かかったか?その回数を返却するメソッドだ。
6種類という言葉を聞いてSetが思い浮かべばしめたものだ。Setはこの種類という言葉ととても相性がいい。
繰り返し乱数を生成し、Setに突っ込みその要素数が6になるまでの回数を求めればよい。重複したデータを追加しても無視されるSetの特性を生かした処理だ。

static String createStar(int count)

実行例を見ると回数の分だけ*を描画している。引数で受け取った数分*を連結した文字列を返すメソッドを作成しよう。

	static String createStar(int count){
		StringBuilder sb=new StringBuilder();
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}

今回は大量に文字列を連結していくことになるのでStringBuilderを利用した。

メインメソッド

このくらいの準備でとりあえず形に出来そうだ。以下のようにメインメソッドを作成する。

import java.util.*;
public class DiceCompApp{
	public static void main(String[] args){
		//<何回でコンプしたか,それが何回あったか>を管理するMap
		//TreeMapを使うことでKeyを昇順にしてくる。
		Map<Integer,Integer> map=new TreeMap<>();
		//実行例をみると1回から~発生した最大回数まで出力しているのでその最大回数を調べる変数
		int maxThrowCount=0;
		for(int i=0;i<500;i++){
			//1回ごとのコンプ回数
			int diceThrowCount=diceCompCount();
			if(diceThrowCount>maxThrowCount){
				//もし、最大回数が更新されたらmaxThrowCountを更新していく
				maxThrowCount=diceThrowCount;
			}
			//Mapに登録するときの回数用の変数
			int count;
			if(map.containsKey(diceThrowCount)){
				//再登場ならばcountは今までの合計値+1
				count=map.get(diceThrowCount)+1;
			}else{
				//初登場ならばcountは1
				count=1;
			}
			//マップに登録(or 更新)
			map.put(diceThrowCount,count);
		}
		System.out.println("***************************結果*****************************");
		for(int i=1;i<=maxThrowCount;i++){
			//マップに含まれているときだけ*を出力する
			System.out.printf("%d(%d):%s%n",i,300*i,map.containsKey(i) ? createStar(map.get(i)):"");
		}
	}
	static String createStar(int count){
		StringBuilder sb=new StringBuilder();
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}
	static int diceCompCount(){
		Random rand = new Random();
		Set<Integer> set=new HashSet<>();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}
}

今回の主役はMap<Integer,Integer>だ。重要な部分はすべてコメントにしたのでしっかりと理解してもらいたい。

実行例

***************************結果*****************************
1(300):
2(600):
3(900):
4(1200):
5(1500):
6(1800):*******
7(2100):******************
8(2400):**************************************
9(2700):********************************
10(3000):*****************************************
11(3300):***************************************************
12(3600):**************************************
13(3900):**************************************
14(4200):***************************************
15(4500):********************************
16(4800):*************************
17(5100):************************
18(5400):******************
19(5700):*****************
20(6000):***************
21(6300):*****
22(6600):********
23(6900):********
24(7200):*******
25(7500):*********
26(7800):******
27(8100):***
28(8400):****
29(8700):****
30(9000):****
31(9300):****
32(9600):
33(9900):*
34(10200):
35(10500):
36(10800):
37(11100):**
38(11400):*
39(11700):
40(12000):
41(12300):
42(12600):
43(12900):
44(13200):
45(13500):
46(13800):
47(14100):*

平均やモードの算出

ここまででメインとなる処理はできた。あとは実行例にあるように平均やモードを求めていこう。

static int calcAvg(Map<Integer,Integer> map)

	static int calcAvg(Map<Integer,Integer> map){
		int totalCount=0;
		int sum=0;
		for(int compCount : map.keySet()){
			totalCount+=map.get(compCount);
			sum+=compCount * 300 * map.get(compCount);
		}
		return sum/totalCount;
	}

マップの情報をもとに、合計と個数を求め平均を求めている。

static int calcMode(Map<Integer,Integer> map)

	static int calcMode(Map<Integer,Integer> map){
		int modeKey=0;
		int modeValue=0;
		for(int compCount:map.keySet()){
			if(modeValue<map.get(compCount)){
				modeValue=map.get(compCount);
				modeKey=compCount;
			}
		}
		return modeKey;
	}

最大回数があった場合にそのときのkeyを更新していく処理だ。
戻り値として最も出現回数の多かったコンプ回数を返す。

メインメソッド

import java.util.*;
public class DiceCompApp{
	public static void main(String[] args){
		Map<Integer,Integer> map=new TreeMap<>();
		int maxThrowCount=0;
		for(int i=0;i<500;i++){
			int diceThrowCount=diceCompCount();
			if(diceThrowCount>maxThrowCount){
				maxThrowCount=diceThrowCount;
			}
			int count;
			if(map.containsKey(diceThrowCount)){
				count=map.get(diceThrowCount)+1;
			}else{
				count=1;
			}
			map.put(diceThrowCount,count);
		}
		System.out.println("***************************結果*****************************");
		for(int i=1;i<=maxThrowCount;i++){
			System.out.printf("%d(%d):%s%n",i,300*i,map.containsKey(i) ? createStar(map.get(i)):"");
		}
		System.out.printf("コンプ平均値:%d円%n",calcAvg(map));
		System.out.printf("モード(最頻値):%d回(%d円)%n",calcMode(map),calcMode(map)*300);
	}
	static String createStar(int count){
		StringBuilder sb=new StringBuilder();
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}
	static int diceCompCount(){
		Random rand = new Random();
		Set<Integer> set=new HashSet<>();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}
	static int calcAvg(Map<Integer,Integer> map){
		int totalCount=0;
		int sum=0;
		for(int compCount : map.keySet()){
			totalCount+=map.get(compCount);
			sum+=compCount * 300 * map.get(compCount);
		}
		return sum/totalCount;
	}
	static int calcMode(Map<Integer,Integer> map){
		int modeKey=0;
		int modeValue=0;
		for(int compCount:map.keySet()){
			if(modeValue<map.get(compCount)){
				modeValue=map.get(compCount);
				modeKey=compCount;
			}
		}
		return modeKey;
	}
}

リファクタリング

以上で仕様を満たす処理を実現できた。これはこれでわかりやすくて良いのだが少しリファクタリングしてみよう。まずは処理速度の改善だ。平均やモードを求める際、その度にループを回している。この処理は実はメインに含めてしまえば必要はない。

無駄なループの排除

import java.util.*;
public class DiceCompApp{
	public static void main(String[] args){
		Map<Integer,Integer> map=new TreeMap<>();
		int maxThrowCount=0;
		int totalCost=0;
		int modeCount=0;
		int mode=0;
		for(int i=0;i<500;i++){
			int diceThrowCount=diceCompCount();
			totalCost+=diceThrowCount*300;
			if(diceThrowCount>maxThrowCount){
				maxThrowCount=diceThrowCount;
			}
			int count;
			if(map.containsKey(diceThrowCount)){
				count=map.get(diceThrowCount)+1;
			}else{
				count=1;
			}
			map.put(diceThrowCount,count);
			if(count>modeCount){
				modeCount=count;
				mode=diceThrowCount;
			}
		}
		System.out.println("***************************結果*****************************");
		for(int i=1;i<=maxThrowCount;i++){
			System.out.printf("%d(%d):%s%n",i,300*i,map.containsKey(i) ? createStar(map.get(i)):"");
		}
		System.out.printf("コンプ平均値:%d円%n",totalCost/500);
		System.out.printf("モード(最頻値):%d回(%d円)%n",mode,mode*300);
	}
	static String createStar(int count){
		StringBuilder sb=new StringBuilder();
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}
	static int diceCompCount(){
		Random rand = new Random();
		Set<Integer> set=new HashSet<>();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}
}

平均値やモード計算に必要な値をすべてmainに記述した。無駄なループがなくなり処理速度がアップした。平均値やモードを求めるメソッドを削除してしまおう。

static final化

実はこの処理はメモリ効率的にかなり無駄な部分がある。たとえばdiceCompCoumtメソッドの中にRandom rand=new Random()という記述がある。これは今回でいえば500回newしてしまうことになる。Randomインスタンスをstaticで宣言してしまえばメモリ確保は1回ですむ。同様にSetやMap、StringBuilderインスタンスもstatic化してしまおう。

import java.util.*;
public class DiceCompApp{
	static final Map<Integer,Integer> map=new TreeMap<>();
	static final Set<Integer> set=new HashSet<>();
	static final Random rand=new Random();
	static final StringBuilder sb=new StringBuilder();
	public static void main(String[] args){
		int maxThrowCount=0;
		int totalCost=0;
		int modeCount=0;
		int mode=0;
		for(int i=0;i<500;i++){
			int diceThrowCount=diceCompCount();
			totalCost+=diceThrowCount*300;
			if(diceThrowCount>maxThrowCount){
				maxThrowCount=diceThrowCount;
			}
			int count;
			if(map.containsKey(diceThrowCount)){
				count=map.get(diceThrowCount)+1;
			}else{
				count=1;
			}
			map.put(diceThrowCount,count);
			if(count>modeCount){
				modeCount=count;
				mode=diceThrowCount;
			}
		}
		System.out.println("***************************結果*****************************");
		for(int i=1;i<=maxThrowCount;i++){
			System.out.printf("%d(%d):%s%n",i,300*i,map.containsKey(i) ? createStar(map.get(i)):"");
		}
		System.out.printf("コンプ平均値:%d円%n",totalCost/500);
		System.out.printf("モード(最頻値):%d回(%d円)%n",mode,mode*300);
	}
	static String createStar(int count){
		sb.setLength(0);
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}
	static int diceCompCount(){
		set.clear();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}
}

これで無駄な処理はなくなった。static化したのでメソッド内で利用するときにはcreateStar内のStringBuilerインスタンスはsb.setLength(0)で
DiceCompCount内のSetインスタンスはset.clear()で要素を削除すること。

MagicNumberの定数化

現在処理の中に出てくる1回引く単価の300や試行回数の500という値はマジックナンバーと呼ばれ定数化したほうがよい。その処理を行おう。

import java.util.*;
public class DiceCompApp{
	static final int PER_PLAY_COST=300;
	static final int TRIAL_COUNT=500;
	static final Map<Integer,Integer> map=new TreeMap<>();
	static final Set<Integer> set=new HashSet<>();
	static final Random rand=new Random();
	static final StringBuilder sb=new StringBuilder();
	public static void main(String[] args){
		int maxThrowCount=0;
		int totalCost=0;
		int modeCount=0;
		int mode=0;
		for(int i=0;i<TRIAL_COUNT;i++){
			int diceThrowCount=diceCompCount();
			totalCost+=diceThrowCount*PER_PLAY_COST;
			if(diceThrowCount>maxThrowCount){
				maxThrowCount=diceThrowCount;
			}
			int count;
			if(map.containsKey(diceThrowCount)){
				count=map.get(diceThrowCount)+1;
			}else{
				count=1;
			}
			map.put(diceThrowCount,count);
			if(count>modeCount){
				modeCount=count;
				mode=diceThrowCount;
			}
		}
		System.out.println("***************************結果*****************************");
		for(int i=1;i<=maxThrowCount;i++){
			System.out.printf("%d(%d):%s%n",i,PER_PLAY_COST*i,map.containsKey(i) ? createStar(map.get(i)):"");
		}
		System.out.printf("コンプ平均値:%d円%n",totalCost/TRIAL_COUNT);
		System.out.printf("モード(最頻値):%d回(%d円)%n",mode,mode*PER_PLAY_COST);
	}
	static String createStar(int count){
		sb.setLength(0);
		for(int i=0;i<count;i++){
			sb.append("*");
		}
		return sb.toString();
	}
	static int diceCompCount(){
		set.clear();
		int count=0;
		while(set.size() < 6){
			count++;
			int dice=rand.nextInt(6)+1;
			set.add(dice);
		}
		return count;
	}
}

完成&考察

以上で完成だ。今回はお題を通してSetやMapといったコレクションの練習をした。
SetやMapの便利さを感じていただけたのではないだろうか?

さて、冒頭のお題「このゲームをやったほうがよいか否か」をプログラミングによる検証の結果を用いて考察してみる。
平均は4400円前後だが、モードは11回(3300円)周辺となっていて微妙なところだ。ただ、揃わないときは70回(21000円)かかるときもあるようなのでやはりやめておいたほうがいいだろう。得するときはわずかで損するときが痛すぎる印象だ。

関連記事

コメント

タイトルとURLをコピーしました