課題1ソフトウェア説明

古いコードは別ページへ

card パッケージ

始めに、カードを表現する。cardパッケージにCardインターフェイスを定義する。

card/Card.java


package card;
public interface Card extends Comparable<Card> , Cloneable {
	public int rate();
	public int id() ;
	public String name();
	public Card clone();
	public void init(int num, String name);
}

カードは名前の他にidとレア度(貴重さを表す数値。大きいほど貴重で、値は 出現確率の逆数に比例する)を返せるようにする。 また、検索や表示のために Comparable インターフェイスを継承する。 さらに、複数のカードを効率よく作るために Cloneable インターフェイスを継承し、 clone メソッドを public で宣言する。 また、clone したオブジェクトを初期化できるように初期化のメソッドも宣言する。

このCardインターフェイスを実装する抽象クラスとして、 AbstractCard クラスを作る。 これは、コンストラクタで id と name を受け入れる。 その値をそのまま返す getter をそれぞれ id(), name() とする。 また name を toString で返す。 Comparable の順序 compareTo を id 順に定義する。 compareTo を実装したので、 equals と hashCode も実装しておく。 なお、 rate は定義していないので、 abstract 宣言をする。

card/AbstractCard.java


package card;
public abstract class AbstractCard implements Card {
	private int id;
	private String name;
	protected AbstractCard(){
	}
	@Override
	public Card clone(){
		try{
			return (Card) super.clone();
		}catch(CloneNotSupportedException e){
			throw new InternalError();
		}
	}
	public final void init(int id, String name){
		this.id=id;
		this.name = name;
	}
	@Override
	public final int compareTo(Card arg0) {
		return id()-arg0.id();
	}
	@Override
	public int hashCode() {
		return id;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (! (obj instanceof Card))
			return false;
		Card other = (Card) obj;
		return this.id()==other.id();
	}
	@Override
	public final String name() {
		return name;
	}
	@Override
	public final int id() {
		return id;
	}
	@Override
	public final String toString() {
		return name;
	}
}

さて、いくらでも引ける、レア度最低=1のカードのクラスとして、Zako クラスを作る。

card/Zako.java


package card;
public class Zako extends AbstractCard {
	public Zako() {
		super();
	}
	@Override
	public int rate(){
		return 1;
	}
}

このクラスのインスタンスを利用して、 Prototype デザインパターンを用い (id=0, name="Aka"), (id=1, name="Ao"), (id=2, name="Kuro") など、パラメータを与えることで、インスタンスの List<Card> 型のイ ンスタンスが getList メソッドで得られるようなクラス CardFactory を作る。 ただし、課題後半で作り方を変えるため、ほとんどの実装は AbstractCardFactory で行い、実際の作成は CardFactory クラスで行う。

card/AbstractCardFactory.java


package card;
import java.util.ArrayList;
import java.util.List;
public class AbstractCardFactory<E extends Card> {
	private int num;
	private String[] names;
	private E card;
	public AbstractCardFactory(int num, String[] names, E card) {
		this.num=num;
		this.names=names;
		this.card=card;
	}
	protected List<E> getList() {
		List<E> list  = new ArrayList<E>();
		int i = num;
		for(String name : names){
			@SuppressWarnings("unchecked")
			E newcard = (E) card.clone();
			newcard.init(i++,name);
			list.add(newcard);
		}
		return list;
	}
}

card/CardFactory.java


package card;
import java.util.List;
public class CardFactory extends AbstractCardFactory<Card> {
	public CardFactory(int num, String[] names, Card card) {
		super(num,names,card);
	}
	@Override
	public List<Card> getList() {
		return super.getList();
	}
}

CardFactory のコンストラクタにカード番号の先頭番号を与え、 名前の配列を与える。 そしてCardを実装したインスタンスを一つ渡す。 getList メソッドは Prototype デザインパターンにより、名前に対する番号 を生成して、各カードのリストを返す。

なお、このように、本課題のプログラムでは id で返す数は各クラスで重複し ないように管理する必要がある。 但し、このような設計は大規模設計ではバグの元に なるので、本来は id もコンピュータで管理させる方が良い。 本課題ではパッケージの簡潔さを優先したので省いた。

gacha パッケージ

さて、ここで、カードデッキを考える。 これは、カードを貯めておくもので、一枚ずつカードを引くことができるもの とする。 なお、これは後に確率分布を変えることを考えるので、確率分布自体をインター フェイスにしておく。

カードをランダムに引くアルゴリズムとして、次のように考える。 Java で簡単に生成できる乱数は java.lang.Math.random() で与えられる、 0 から 1 までの一様分布である。 従って、様々な分布でカードを引く場合、所望の出現割合に比例配分をするこ とを考える。 つまり、例えば、3種類のカード (a,b,c) のレア度が (1,1,10) であるとする。 すると、a,b に対して c が10倍出にくいとする。 つまり、出現割合はこの逆数で (1,1,1/10)となる。すると、これを0から1の 一様分布に対応付けると、(10/21,10/21,1/21) という確率になる。 これを小数第三位までの数で考えると、区間として (0.000-0.476,0.476-0.952,0.952-1.000) を考えて、0.000から1.000未満の数 が与えられたらその値がどこの区間に入るかを検索するようにする。

この仕組みとして、 java.util.TreeMap<Double,Card> を作成し、得ら れた乱数に対して、floorEntry メソッドを利用して区間を検索することとす る。 つまり、上記の例であれば、 Card 型のインスタンス a,b,c に対して、 TreeMap のインスタンス map に (a,0), (b, 0.476), (c,0.952) が登録 されていれば、0から 1 までの一様分布の確率変数 x により、 map.floorEntry(x) を求めれば、所望の確率分布で a,b,c が得られる。

Deck はカード種を管理し、所持しているカード種から、この TreeMap を計算 する。 一方、 Bunpu は Deck より確率分布を生成し、ランダムに引く仕組みを提供 する。 本来は一体のクラスで作成しても良いのだが、様々な仕組みの確率分布やデッ キを考えたいので、分離して抽象化し、ストラテジデザインパターンを利用する。 その元で Bunpu はコンポジションとして、利用者から Deck の中に隠蔽(カ プセル化)する。

まず、Deck と Bunpu のインターフェイスを定義する。

gacha/Deck.java


package gacha;
import java.util.TreeMap;
import card.Card;
public interface Deck {
	public Card draw();
	public TreeMap<Double, Card> calcMap();
}

draw メソッドでカードを一枚引く。 calcMap メソッドは Bunpu クラスに TreeMap を提供するメソッド であるので、本来の設計思想としては public にすべきではないが、課題にお いてテスト環境を別パッケージにするために public にしてある。

gacha/Bunpu.java


package gacha;
import card.Card;
public interface Bunpu{
	public Card draw();
	public void initMap();
	public Card randomDraw();
}

draw メソッドで、カードを一枚引く。 但し、カードを引く前に、内部の TreeMap の初期化などができるように initMap を用意する。 また、単純に内部の TreeMap を用いてランダムにカードを引く操作を randomDraw メソッドにしておく。 これらは利用者から見える必要は無いので、本来は public にする必要は無い が、課題とテスト環境のために public に設定してある。

次に、 Bunpu の draw 以外を素直に実装した AbstractBunpu クラスを示す。

gacha/AbstractBunpu.java


package gacha;
import java.util.Map;
import java.util.TreeMap;
import card.Card;
public abstract class AbstractBunpu implements Bunpu {
	protected final Deck deck;
	public AbstractBunpu(Deck deck) {
		this.deck = deck;
	}
	protected TreeMap<Double,Card> map;
	@Override
	public void initMap() {
		map =  deck.calcMap();
		if(test.Constant.debug)System.out.println(map);
	}
	@Override
	public Card randomDraw() {
		double rand = Math.random();
		if(test.Constant.debug)System.out.println(rand);
		Map.Entry<Double, Card> e = map.floorEntry(rand);
		if(test.Constant.debug)System.out.println(e);
		return e.getValue();
	}
	@Override
	public String toString() {
		return this.getClass().getName()+" [" + map + "]";
	}
}

一方で、確率分布を設定し、カードを引かせる Deck の抽象クラス AbstractDeck を定義する。 このクラスでやっていないことはカード種の設定である cards の指定と、 bunpu フィールドの設定である。


package gacha;
import java.util.List;
import java.util.TreeMap;
import card.Card;
public abstract class AbstractDeck implements Deck {
	protected List<Card> cards;
	protected Bunpu bunpu;
	public AbstractDeck(List<Card> list){
		super();
		cards = list;
	}
	public final double total() {
		double total = 0;
		for(Card card : cards){
			double rate = getCardRate(card);
			if(rate!=0.0){
				total += rate;
			}
		}
		return total;
	}
	@Override
	public final Card draw(){
		return bunpu.draw();
	}
	public final double getCardRate(Card card){
		return 1.0/card.rate();
	}
	@Override
	public final TreeMap<Double,Card> calcMap(){
		TreeMap<Double,Card>map = new TreeMap<Double,Card>();
		double total = total();
		double rate = 0;
		for(Card card : cards){
			double nextRate = getCardRate(card);
			if(nextRate!=0.0){
				map.put(rate, card);
				rate += nextRate/total;
			}
		}
		return map;
	}
	@Override
	public final String toString() {
		return  cards.toString();
	}
}

注: Bunpu のコンストラクタには生成した Deck オブジェクト(this)を持たせるた め、 Deck のコンストラクタの引数には Bunpu オブジェクトを指定できない。

コンストラクタでDeck を受け取ったら、AbstractBunpu のコンストラクタに 渡し、TreeMap を初期化し、 draw メソッドでは単純に randomDraw を呼び出 すだけの Bunpu1 クラスを次に示す。

gacha/Bunpu1.java


package gacha;
import card.Card;
public class Bunpu1 extends AbstractBunpu {
	public Bunpu1(Deck deck){
		super(deck);
		initMap();
	}
	@Override
	public Card draw(){
		return randomDraw();
	}
}

また、 Zako クラスのカード種を生成する getList メソッドと、それらのカー ドに対して、確率分布を Bunpu1 で与える Deck1クラスを示す。

gach/Deck1.java


package gacha;
import java.util.List;
import card.Card;
import card.CardFactory;
import card.Zako;
public class Deck1 extends AbstractDeck {
	public Deck1(){
		super(getList());
		bunpu = new Bunpu1(this);
	}
	public static List<Card> getList(){
		List<Card> list=(new CardFactory(0,new String[]{"Aka", "Ao", "Kuro"},new Zako())).getList();
		return list;
	}
}

player パッケージ

このパッケージではデッキからカードを引いて集計する。 まず、引いたカードを蓄えるクラス CardCollection を考える。 これは add メソッドでカードを加えると、 toString メソッドでカードごと の集計結果を出力すると言うものである。 また、あるカードを持っているかを調べる contains メソッドと、特定のカー ド種が全部揃っているかを調べる completes メソッドも実装する。

player/CardCollection.java


package player;
import java.io.StringWriter;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import card.Card;
public class CardCollection {
	private Map<Card,Integer> map = new TreeMap<Card,Integer>();
	public CardCollection(){
	}
	public void add(Card card){
		if(map.containsKey(card)){
			map.put(card, map.get(card)+1);
		}else{
			map.put(card, 1);
		}
	}
	public boolean contains(Card card){
		return map.containsKey(card);
	}
	@Override
	public String toString() {
		if(map.isEmpty()) return "nothing.";
		StringWriter sw = new StringWriter();
		int counter=0;
		for(Map.Entry<Card, Integer> e : map.entrySet()){
			if(counter++>0){
				if(counter<map.size()){
					sw.write(", ");	
				}else{
					sw.write(" and ");
				}
			}
			sw.write(e.getValue()+" "+e.getKey()+"(s)");
		}
		return sw.toString();
	}
	public boolean completes(Set<Integer> watch) {
		int num =0;
		for(Card card : map.keySet()){
			if(watch.contains(card.id())){
				num++;
			}
		}
		return num==watch.size();
	}
}

さて、一枚ずつデッキからカードを引いて CardCollection に蓄える Player クラスを作成する。 一枚引いて CardCollection に蓄えるだけのメソッドを singleDraw とする。 さらに、Enter キーを押す度に一枚カードを引いては集計結果を出力する。カー ドがなくなるか、EOF(Ctrl-Z)が入力されたら終了するというメソッドを play とする。 なお、CardCollection のインスタンスは public にしておく。

player/Player.java


package player;
import gacha.Deck;
import java.io.IOException;
import card.Card;
public class Player {
	public CardCollection collection;
	public Player(){
		collection = new CardCollection();
	}
	public void play(Deck deck) throws IOException {
		message();
		int c;
		while((c=System.in.read())!=-1){
			if(c=='\n'){
				Card card = singleDraw(deck);
				System.out.println("I got "+card+".");
				System.out.println("I have "+collection+".");
				if(test.Constant.debug)System.out.println(deck);
				message();
			}
		}
		System.out.println("Game over");
	}
	public Card singleDraw(Deck deck){
		Card card = deck.draw();
		collection.add(card);
		return card;
	}
	private void message() {
		System.out.println("Push Enter Key");
	}
}

さて、以上でカードを引いて集める仕組みが整ったため、main メソッドを作 成する。 Main1 クラスでは、 Deck1 クラスのカードを引いて集める。

player/Main1.java


package player;
import java.io.IOException;
import gacha.Deck1;
public class Main1 {
	public static void main(String[] args) throws IOException {
		new Player().play(new Deck1());
	}
}

さて、最後に指定したカード種を全部集めるまでカードを引き続け、回数を出 力する Statictics クラスを与える。これは、集めるべきカード種の id の配 列と、 Player インスタンスと Deck インスタンスを与え、 calc メソッドで 回数を得る。 与え方は、継承してコンストラクタの引数とする。 なお、集計用に頻度ごとに並べる Hindo クラスを定義した。

player/Statistics.java


package player;
import gacha.Deck;
import java.util.HashSet;
import java.util.Set;
import card.Card;
public class Statistics {
	private Player player;
	private Deck deck;
	private Set<Integer> watch;
	public Statistics(int[] watchList, Player player, Deck deck){
		this.player = player;
		this.deck = deck;
		watch = generateWatchSet(watchList);
	}
	public void calc(){
		int round =0;
		Hindo hindo = new Hindo();
		while(!player.collection.completes(watch)){
			round++;
			Card card = player.singleDraw(deck);
			System.out.println(card);
			if(!hindo.containsKey(card)){
				hindo.put(card, round);
			}
		}
		System.out.println("total: "+round);
		System.out.println(hindo);
	}	
	private Set<Integer> generateWatchSet(int[] watchList) {
		Set<Integer> watch = new HashSet<Integer>();
		for(int i : watchList){
			watch.add(i);
		}
		return watch;
	}
}

player/Statistics1.java


package player;
import gacha.Deck1;
public class Statistics1 {
	public static void main(String[] arg){
		int[] watchlist = {0,1,2};
		Statistics s = new Statistics(watchlist, new Player(), new Deck1());
		s.calc();
	}	
}

player/Hindo.java


package player;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import card.Card;
public class Hindo extends HashMap<Card, Integer> {
	private static final long serialVersionUID = 1L;
	public Hindo(){
		super();
	}
	@Override
	public String toString() {
		List<Map.Entry<Card,Integer>> list = new ArrayList<Map.Entry<Card,Integer>>(entrySet());
		Collections.sort(list, new HindoComparator());
		return list.toString();	
	}
	class HindoComparator implements Comparator<Map.Entry<Card,Integer>>{
		@Override
		public int compare(Entry<Card, Integer> arg0, Entry<Card, Integer> arg1) {
			return arg0.getValue()-arg1.getValue();
		}
	}
}

以上で、基本的なクラスの説明を終わる。 上記のうち、 Main1 クラスと Statistics1 クラスは実際に実行できる。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科