インタラクティブな絵本『Cinderella』

iPad insightで紹介されていた絵本『シンデレラ』
http://ipadinsight.com/ipad-app-reviews/ipad-books-for-kids-cinderella-by-nosy-crow


絵のカワイさに半ば一目惚れ。この記事見た瞬間から早く触ってみたくて仕方がなかった。
値段は$7.99で、日本のiTunesStoreだと現在は700円。


Let's play!



この絵に惹かれた。素敵でしょ?
モードは朗読&遊ぶ、朗読のみ、自分で朗読(要は音声なし)の3種類。朗読は男の子がしてくれます。
いやいやいやいや、シンデレラの台詞は女の子が読んでくれよ…
全部英語だけどそんなに難しくはない。たまにわかんない単語が出てくるけどだいたい言ってることはわかる。



画面上の物を触れます。人にタッチすると喋る。台詞も何種類か用意されている。
「お皿を食器棚に片付けてくれないか?」と言われたのでお皿をドラッグして片付けた後の図。
いや、それはアナタの仕事で(略)




舞踏会に招待されて準備しているところ。
義姉に「ブレスレットを探してこい」だの「髪飾りを探して来い」だの言われる。部屋から探してドラッグしてあげましょう。



かくかくしかじかでシンデレラも舞踏会に行くことに。
魔法使いのおばあさんが3匹のネズミとカボチャを所望じゃ。何が始まるんだろう。
探してきてドラッグ。



インタラクティブな絵本の最大の見せ場の1つ。
おばあさんにタッチするとシンデレラの衣装が変わる!舞踏会に行く衣装を選ぼう。



舞踏会にて。
最大の見せ場その2。音符にタッチするとダンスとBGMが変わる!



問題の場面


紙の本を電子化しただけだと値段が紙の本より安くないと納得できないけど、こういう付加価値があるならいいよね!
絵本の中を触れるってだけで楽しいんだもの。


オフィシャルな動画で子どもが動かしてる様子が見れます。

音楽もとても素敵です♪


iTunesStoreへのリンク

Lion MacPortsでncursesのインストールに失敗した。

LionでMacPortsphpMyAdminをインストールしたらncursesのインストールで失敗した。


こんなログがでる。

    • > Building ncurses

Error: Target org.macports.build returned: shell command failed (see log for details)
Error: Failed to install ncurses
Log for ncurses is at: /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_release_tarballs_ports_devel_ncurses/ncurses/main.log
Error: The following dependencies were not installed: php5-gd php5 apache2 apr-util sqlite3 libedit ncurses openssl pcre perl5 perl5.12 gdbm perl5 perl5 perl5 autoconf213 gawk gettext readline m4 gsed libtool automake autoconf help2man p5.12-locale-gettext libxml2 mhash pkgconfig glib2 php5-mbstring php5-mcrypt libmcrypt php5-mysql php5-zip
Error: Status 1 encountered during processing.
To report a bug, see http://guide.macports.org/#project.tickets


個別にncursesを再インストールしたら直った。
$ sudo port clean ncurses
$ sudo port install ncurses


Snow Leopard時代にMacPortsでインストールしたncursesが悪さをしていたのかも。MacPortsで色々苦労してるからHomeBrewにしたいとこだけど、すでにいろいろMacPortsで入れたからな〜。

モックフレームワークJMockitで実行時にクラスを差し替える

テストをしたいのに連携システムが起動していなくて単体でのテストができませんでした。テストをしにくいという設計上の問題点はおいといて、他システムと連携する部分をモックで差し替えて動かせないものかと、Javaのモックフレームワークを調べると、EasyMock, JMock, Mockitなどが検索でヒットします。djUnitもモックフレームワークの一種でしょうか。調べた中で一番使いやすそうだったJMockitについてのメモを書いておきます。引数によってモッククラスの挙動を変更する、というのが他のモックフレームワークでは難しそうでした。


JMockitのページはこちら。
Google Code Archive - Long-term storage for Google Code Project Hosting.
Java 1.5以上で動くようです。ダウンロードしたらクラスパスにjmockit.jarを追加します。1.5で動かす場合はVMの引数に -javaagent:jmockit.jar を指定する必要があります。


サンプルコード

モック対象のクラス。


public class SayHello {

public String echo(String s) {
return "Hello!";
}
}


モック。SayHelloをこのクラスで差し替えます。


import mockit.*;

@MockClass(realClass=SayHello.class)
public class MockSayHello {

@Mock
public String echo(String s) {
return s;
}
}

MockClassアノテーションでモッククラスであることを定義し、属性のrealClassでモック対象のクラスを指定します。Mockアノテーションでモックメソッドであることを定義します。


サンプルのメインクラス。


public class MockSampleRunner {

public static void main(String[] args) {
Mockit.setUpMock(MockSayHello.class);

SayHello obj = new SayHello();
System.out.println(obj.echo("Mocked echo"));
}
}

モックを実行する前にMockitクラスのstaticメソッド
public static void setUpMocks(Object... mockClassesOrInstances)
でモッククラスを設定する必要があります。

実行結果

Mocked echo


SayHello.echo() の振る舞いがモッククラスによって差し替えられています。

インスタンス初期化子と2重ブレースによる初期化

開発者のためのプログラミングQ&Aサイト stackoverflow の Hidden Features of Java というエントリに「Double Brace Initialization」という機能が載ってました。知らなかったので調べたことをまとめます。Double Brace Initialization はインスタンス初期化子のシンタックスシュガーのようです。たぶん…。

インスタンス初期化子(Instance Initializer)

スタティック初期化子とは別にインスタンス初期化子というものがある。コンストラクタの前に実行される。


public class InstanceInitializer {
private int num;

static {
System.out.println("from static initializer");
}

// インスタンス初期化子
{
System.out.println("from instance initializer");
num += 1;
}

public InstanceInitializer() {
num += 1;
System.out.println("from constructor");
}

public static void main(String[] args) {
InstanceInitializer obj = new InstanceInitializer();
System.out.println("num: " + obj.num);
}
}

【出力結果】


from static initializer
from instance initializer
from constructor
num: 2

2重ブレースによる初期化


public static void main(String[] args) {

JFrame frame = new JFrame("Sample") {{
// 2重ブレースによる初期化
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setSize(100, 100);
getContentPane().add(new JButton("button"));
}};

frame.setVisible(true);
}

外側のブレースがクラス定義のブレースで内側のブレースがインスタンス初期化子。匿名インナークラスのインスタンス初期化子で、JFrameのメンバメソッドを呼び出して初期化しています。

【出力結果】

コロプラ 序盤講座

コロプラ」は携帯でできるGPSを使ったゲームです。正式名称を「コロニーな生活☆PLUS」と言います。iPhone対応されたので始めました。やってみるとわかりますがなぜか序盤が辛いゲームです。 序盤に必要な知識と攻略法を私なりにまとめます。

■ターンがいつ進むのか

1時間経過すると1ターン進みます。自分の「出来事」欄で、朝の感謝プラを何分に貰っているのかを確認しましょう。その時刻が、ターンが進む時刻になります。

■資源がすごい減ってくんですが…

人口と施設のバランスが取れていません。初期のフィールドは、農場、貯水池が3つずつぐらいが適切かもしれません。 人口が増えすぎた場合は、住居の場所に空き地(10)を作って施設を建てるという対処もアリ。

■隕石とは

通常の隕石とアイテム隕石があります。「10ターン後ダメ隕石」とは約10時間後に通常の隕石が落ちるよ、という意味になります。隕石の発生は10ターン前から。アイテム隕石は墜落したらアイテムが貰えます。どちらも墜落した場所のマスが土地ごと無くなります。

■隕石の対処

方法1:位置登録で1Km以上移動
発生した隕石が無かった事になります。「アイテム隕石です」と言われてるのに移動すると、発生していたアイテム隕石は無くなります。


方法2:ミサイル(30)を設置してONにする
アイテム隕石の場合は迎撃するとアイテムが貰えないので注意。「アイテム隕石です」と言われてる場合はOFFにしましょう。


方法3:避雷針(50)を設置する
必ずその場所に隕石が墜落します。墜落したら避雷針と土地は無くなります。


ミサイルと避雷針を設置した場合はミサイルが優先されます。


上記で一番安全なのは避雷針の設置ですが、避雷針(50) + 土地(80)でプラが150かかってしまいます。初期は、ダメ隕石は位置登録で、位置登録できない時はミサイルで回避、アイテム隕石は運に任せて受け止める、ってのがお金がかからなくて良いかもしれません。
特に勤務時間中はコロプラをこまめにやってる時間なんて無いと思うので、職場についてもすぐに位置登録をせずに、お昼休みに隕石情報を確認してから位置登録する、というワザもアリ。

■レーダー?

隕石の発生を早く検知するための施設です。レーダー0機だと、自分で隕石の墜落を検知できるのは2ターン前からです。1機増設すると1ターン早く検知できるようになります。最大で8機まで効果があります。9機以上は意味がありません。
レーダーで他のコロニーの隕石発生も同じように早く検知できるようになります。ご近所のコロニーさんが教えてくれるのはこのためなんですねぇ。
でもプラが高いので序盤は無くても良いかも。プラを投資するだけの効果が見込めません。隕石が墜落して施設や住居が破壊されても復帰にそれほどはプラがかからないのと、土地の上ではなく空に落ちる確率が高いからです。

■他のコロニーから補給があんまり受けられなくなったけど…

初めてから2週間経つと初心者コロニーではなくなります。若葉マークが消えてご近所コロニーの中で目立たなくなるのです。つまり、それまでのようには頻繁に補給を受けられなくなります…
特に朝8時台のターンで資源が枯渇していると、朝の感謝プラがもらえません ヤバそうだったら、移動距離が0でも8時前に位置登録しておきましょう。ご近所コロニーは位置登録した順番に上から表示されるので、補給してくれる確率が上がります。

■自給自足への道

施設を増やしても人口増に対処できないじゃんと思っているそこのア・ナ・タ。その通りですw。施設だけでは自給自足の達成は困難です。アイテムを設置しましょう。アイテムは施設と同じようにフィールドにセットして初めて効果が現れます。


「雪解け水の春」「光合成の夏」「豊作の秋」
というセット系アイテムがそれぞれ、貯水池、木、農場の生産量を増やしてくれます。
「雪解け水の春Lv1」は生産量が2倍になります。Lv2だと3倍。Lv10は11倍です。


Lv50以降を目標に手に入れたいところです。アイテム市場で買いましょう。 Lv50を買うとしたらだいたい400プラが目処でしょうか。アイテム市場は今この瞬間にも市場経済が働いているので、400プラでLv30以下のモノしか並んでないこともあれば、400プラでLv80ぐらいのモノが並ぶこともあります。タイミングを見計らってできるだけ高いLvのものを手に入れたいですね。
以上が生産量を増やすアイテムですが、それぞれを400プラで買うとしたら1200プラかかります(!) それまで資源が枯渇するかも知れないという不安な毎日を過ごすの!?と思ったアナタは「広大な大地」というアイテムを最初に手に入れましょう。
「広大な大地」は資源の貯蔵量を増やしてくれます。これまでの計算と同じように、広大な大地Lv5は貯蔵量が6倍になります。


貯蔵量が増えただけでは自給自足にとっては何の足しにもなりませんが、他コロニーからの補給をそれだけ貯めておけますw。ですが、これまで紹介したアイテムと同じように「広大な大地Lv50」を最初に設置してしまうと他コロニーの印象がよくありません。たぶん…。
既に貯水池が2つあって貯蔵量が1000の場合、「広大な大地Lv50」を設置すると、貯蔵量が51000になります。
どんだけ補給してもらう気だよ!、って思われるかもw
Lv5〜Lv10ぐらいを同じく400プラぐらいで手に入れたいところです。 貯蔵量が10000ぐらいになるLvが安心でしょうか。

■テーマって?

コロニーの壁紙のようなものです。設定するとそのテーマに合わせた施設/背景の画像になります。
気分転換にどうぞ♪こ近所コロニー覗いてるとたまにステキなテーマがありますネ

■アイテムを売りたい!

使わないテーマとか、いらないアイテムを売ってプラを増やしたいですね。アイテムを売る方法は2つあります。


その1:アイテム市場に出品
売りたいアイテムの相場を見て値段を決めて出品します。 出品料として売りたい値段の5%が最低限余分に必要になります。市場の底値より低ければすぐに買い手がつきます。


その2:倉庫販売
手に入れたアイテムは「Myアイテム」という場所に保存されています。 これを「倉庫」に移動してから値段をつけて販売することができます。
出品料はかかりませんが、マイコロニーを訪問してくれた人にしか商品は見えません。
全然売れない場合は「状態」欄に「〜をXXプラで売ってます。」と書いたり、取引掲示板に書き込みをしたりしてアピールしましょう。

任意の Kind の Entity を全て削除する

Kind の Entity を削除したい時に、Dashboard からだとチマチマしか削除できないので全件削除する Servlet を書いた。
DataStoreCleaner が DataStore にアクセスして Entity の削除を行うクラス。
DataStoreCleanServlet が Servlet
TaskQueue を使って Entity が全てなくなるまで削除を繰り返す。
1回のタスクで100件削除する。まだ Entity が残っている場合は TaskQueue で削除処理を継続する。


動作確認した環境:
 Eclipse3.5 + jre1.5.0_16 + appengine-java-sdk-1.3.0


Production 環境で試したところ、Property が本のタイトルのみという Kind で100件削除するのは1秒ぐらいだった。

DataStoreCleaner.java


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

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.FetchOptions.Builder;

/**
* 指定された Kind の Entity を削除します。
*/
public class DataStoreCleaner {
private static final DatastoreService _dstore;
private final String _kind;

static {
_dstore = DatastoreServiceFactory.getDatastoreService();
}

public DataStoreCleaner(String kind) {
_kind = kind;
}

/**
* 指定された件数の Entity を削除します。
* @param cnt 削除する件数
* @return 実際に削除した件数
* @throws IllegalStateException
*/
public int clean(int cnt) {
PreparedQuery pq = _dstore.prepare(new Query(_kind));
FetchOptions options = Builder.withLimit(cnt);
List entities = pq.asList(options);
List keys = toKeyList(entities);
_dstore.delete(keys);
return keys.size();
}

/**
* Entity がまだ残っている場合は true を返します。
* @throws IllegalStateException
*/
public boolean hasLeft() {
PreparedQuery pq = _dstore.prepare(new Query(_kind));
return pq.countEntities() > 0;
}

private List toKeyList(List entities) {
List ret = new ArrayList(entities.size());
for (Entity entity : entities) {
ret.add(entity.getKey());
}
return ret;
}
}

DataStoreCleanServlet.java


import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import DataStoreCleaner;
import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskOptions;

public class DataStoreCleanServlet extends HttpServlet {

private String _queue;

@Override
public void init(ServletConfig config) throws ServletException {
_queue = config.getInitParameter("queue");
if (_queue == null) {
_queue = Queue.DEFAULT_QUEUE;
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

String[] kinds = req.getParameterValues("kind");
if (kinds == null) {
return;
}

int cnt = getDeleteCount(req);
for (String kind : kinds) {
clean(req.getRequestURI(), kind, cnt);
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

String uri = req.getRequestURI();
String kind = req.getParameter("kind");
String cnt = req.getParameter("cnt");
clean(uri, kind, Integer.parseInt(cnt));
}

private void clean(String uri, String kind, int cnt) {
boolean hasLeft = cleanStore(kind, cnt);
if (hasLeft) {
enqueueTask(uri, kind, cnt);
}
}

private int getDeleteCount(HttpServletRequest req) {
String cnt = req.getParameter("cnt");
if (cnt == null || cnt.length() == 0) {
return 500;
}
return Integer.valueOf(cnt).intValue();
}

private boolean cleanStore(String kind, int cnt) {
DataStoreCleaner cleaner = new DataStoreCleaner(kind);
int nDelete = cleaner.clean(cnt);
return cleaner.hasLeft();
}

private void enqueueTask(String url, String kind, int cnt) {
TaskOptions ops = createOptions(url, kind, cnt);
Queue q = QueueFactory.getQueue(_queue);
q.add(ops);
}

private TaskOptions createOptions(String url, String kind, int cnt) {
TaskOptions ops = TaskOptions.Builder.url(url);
ops = ops.param("kind", kind);
ops = ops.param("cnt", "" + cnt);
return ops;
}
}

URL

servlet-name を "clean" にした場合
http://anywhere.appspot.com/clean?kind=kind名

kind は複数指定も可
http://anywhere.appspot.com/clean?kind=kind1&kind=kind2

web.xml

DataStoreCleanServlet は起動時の引数で削除処理に使う TaskQueue の名前を指定できる。
省略した場合はデフォルトのキューを使用する。

【サンプル】



DataStoreCleaner
DataStoreCleanServlet


queue
clean


DataStoreCleaner
/clean

定数フィールドへの参照はコンパイル時にバイナリに展開される

最近知ったこと。
いや、『Java魂』(O'REILLY)を読んだから知ってるはずだけど忘れていた事実。



public static final String HOGE = "hoge";
のような定数への参照は、コンパイル時にバイナリファイルにインライン展開される。
HOGE を参照している箇所は、"hoge" という値が直接バイナリファイルの該当箇所に書き込まれる。)


この仕様は、バイナリ互換性を保つための要件として Java の言語仕様で記述されている。

『The Java Language Specification, Third Edition』
http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html


「13.1 The Form of a Binary」 より
References to fields that are constant variables are resolved at compile time to the constant value that is denoted. No reference to such a constant field should be present in the code in a binary file
(定数のフィールドへの参照はコンパイル時に定数のフィールドが示している値に解決される。そのような参照はバイナリファイルのコード中にあるべきではない。)


言語仕様では、final で宣言され、コンパイル時に初期化されるプリミティブ型もしくは String 型の変数を "定数(constant variable)" と呼んでいる。(「4.12.4 final Variables」 より)
インライン展開を求める理由の1つとして、switch 文の case 値に重複がないことをコンパイル時にチェックするためだと記述されている。(「13.4.9 final Fields and Constants」 より)


定数の値を変更した場合は特に注意。定数フィールドが定義されているクラスはコンパイルされても、その定数を参照しているクラスはコンパイルされない、といったことが起こりえる。Eclipseコンパイラは定数フィールドが変更されたら、それを参照しているクラスを検出して自動でコンパイルするみたいだけど、Sun の javac による差分コンパイルは、ソースファイルとクラスファイルの新旧を比較して、クラスファイルが古い場合に再コンパイルを行うだけ。


この問題の解決策として言語仕様では
・絶対に変更されない値を定数値として使用する。
・アクセス用メソッドを用意する。
を推奨している。

アクセス用メソッドとは、


private static final String HOGE = "hoge";
public static String getHoge() {
return HOGE;
}
のようなもの。めんどくさいけど。

値を変更しない。全部コンパイルし直す。の方が楽ですかね。