[WordPress]PHP5.6にVersionUp時、管理画面の右側が真っ白になったらすべきこと

古いバージョンのWordPressを使用していて、PHPのバージョンを5.6にアップデートすると、管理画面が表示されなくなる場合があります。
左のメニューは表示されるが、右側の操作エリアが真っ白になる場合は、下記の変更をする事で表示されるかもしれません。


wp-admin/includes/screen.phpの706行目

<?php echo self::$this->_help_sidebar; ?>




<?php echo $this->_help_sidebar; ?>




この症状の場合、エラーログを表示させると下記のエラーが出てます。

Fatal error: Access to undeclared static property: 
WP_Screen::$this in blog/wp-admin/includes/screen.php on line 706


[lolipop]WordPressを無料で毎日自動バックアップする


ロリポップでは簡単インストール機能を使って、WordPressをインストールが可能です。
ですが、インストール後の状態ではバックアップが取得されていない状態です。

ロリポップが提供しているバックアップサービスを利用すれば7日分のバックアップ取れますが、最上位のエンタープライズプラン以外では毎月300円の費用が追加で必要です。

このため今回は、ロリポップで提供されているsshアクセスとcron設定機能を利用し、作成したWordPressサイトを無料で自動バックアップできるよう設定を行います。

バックアップは7日分取得し、1週間以上前のデータは自動で上書きするようにします。
(後述のスクリプトの内容を変更すれば期間を30日分に変更したり、毎月月初にバックアップ取得して1年分のバックアップを保存する事も可能です)

WordPressのインストール先を確認する

まず、バックアップしたいWordPressがある場所を確認します。

lolipopのレンタルサーバでは、webサーバからアクセス可能なディレクトリが~/webの下になります。
インストールしたWordPressはこの下にあるはずなので、インストール先を確認します。

インストール場所を覚えていない場合は、下記のコマンドで検索できます。

find ~/web | grep "wp-config\.php$"
/home/users/0/username/web/blog/wp-config.php


上記の例だと、インストール先が/home/users/0/username/web/blogであることが分かります。

本記事では、/home/users/0/username/web/blogフォルダにwp-config.phpがあったものとして、説明を行っていきます。

まんがでわかるLinux

データベースの接続設定を確認する

WordPressは記事の保存にMySQLを使っています。
先ほど調べたwp-config.phpファイルの中にDBの接続情報があるので、下記のコマンドでデータベースの接続設定を検索します。

/home/users/0/username/web/blog/wp-config.php | grep DB
 
define('DB_NAME', 'LA99999-dbname');
define('DB_USER', 'LA99999');
define('DB_PASSWORD', 'dbpassword');
define('DB_HOST', 'mysql999.phy.lolipop.jp');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');



上記の場合、下記のデータベースの接続設定は下記のとおりです。

DBサーバ             mysql999.phy.lolipop.jp
DB名                 LA99999-dbname
DBログインユーザ     LA99999
DBログインパスワード dbpassword




手動でバックアップを取得する

これまでの確認で、バックアップに必要なインストール先とDBの接続情報が集まったので、まずは一旦、手動でバックアップを取得してみます。

バックアップ先はどこでも良いのですが、今回はホームディレクトリの下にwp-backupフォルダを作成して、その中に格納します。

~/webディレクトリの下にバックアップすると、他のユーザからもブラウザからバックアップデータにアクセスできてしまいます。
このため、~/webの下にはバックアップしないようにしてください。

下記のコマンドで、バックアップフォルダを作成します。

mkdir ~/wp-backup



次に下記のコマンドでバックアップを取得します。
1行目のtarコマンドで、WordPressのプログラム自体とアップした画像ファイルなどをパックアップし、2行目のmysqldumpでデータベースのバックアップを採取しています。
インストール先や、DBの接続情報は調査した結果の値に書き換えてください。

tar -zcvf ~/wp-backup/blog_backup.tgz ~/home/users/0/username/web/blog/*
mysqldump --default-character-set=binary -h mysql999.phy.lolipop.jp -u LA99999 -pdbpassword LA99999-dbname | gzip -9 > ~/wp-backup/blog_dbbackup.sql.gz



バックアップが完了したら、lsコマンドで、ファイルができていることを確認します。

ls -lh ~/backup
total 228M
-rw-r--r-- 1 username LolipopUser 226M Feb 20 22:16 blog_backup.tgz
-rw-r--r-- 1 username LolipopUser 707K Feb 20 22:16 blog_backup.tgz.log
-rw-r--r-- 1 username LolipopUser 2.6M Feb 20 22:16 blog_dbbackup.sql.gz



取得したバックアップはFTPで手元にダウンロードし、Lhaplusなどの解凍ツールで展開できることを確認してください。


バックアップの取得スクリプトを作成する

手動でのバックアップに成功したら、次はこの作業をスクリプトファイルに置き換えます。
同時に、世代バックアップの仕込みも行っておきます。

まずは、バックアップスクリプトを保存するために~/web/scriptフォルダを作成し、アクセス制限をかけておきます。
このフォルダの中に.htaccessファイルを作成し、下記の1行を書いておくことでブラウザ経由でアクセスされないようにします。
web/script/.htaccess

deny from all



次にこのフォルダ内に、backup.shのファイル名で下記のスクリプトファイルを作成します。
viエディタが使える場合はviで作成しても良いですし、使えない場合は一旦ftpで移動させてください。
改行文字は、Windows標準のCR+LFでは無くLFで保存します。

#!/bin/bash
 
# backup blog
tar -zcvf ~/wp-backup/blog_backup_`date '+%w'`.tgz ~/home/users/0/username/web/blog/* > ~/wp-backup/blog_backup_`date '+%w'`.tgz.log
mysqldump --default-character-set=binary -h mysql999.phy.lolipop.jp -u LA99999999 -pdbpassword LA99999999-dbname | gzip -9 > ~/wp-backup/blog_dbbackup_`date '+%w'`.sql.gz



スクリプトの中に下記のコマンドがありますが、この部分は実行すると曜日を表す数字に置き換わります。

`date '+%w'`



数字は0~6で、0が日曜日、6が土曜日になります。
実際にどうなるかは、echoコマンドで確認可能です。

$ echo `date '+%w'`
6
 
$ date
Sat Feb 20 23:02:48 JST 2016



スクリプトファイルを作成したら、スクリプトに実行許可を付与します。

chmod 700 ~/web/script/backup.sh




作成したスクリプトを手動実行し、バックアップが採取できていることを確認します。

~/web/script/backup.sh




今回は曜日で7世代バックアップしたいので、”+%w”としましたが、”+%d”とすればこの部分が日付(01~31)になるため、1か月分のバックアップが取得できます。
また、この方法だと、バックアップは一週間前のデータが順次上書きされる為、古いバックアップファイルの消しこみ処理は不要です。

バックアップ取得のスクリプトを自動実行する

最後にlolipopの管理画面より、cron設定で作成したスクリプトを自動実行させます。

ログイン後のメニューより、Webツール->cron設定を選択します。


以下のような感じで設定を行い、作成したスクリプトを毎日決まった時間に実行させます。
何時でも良いですが、アクセスが少ない午前3時あたりに設定しておくと安心です。

※初めて設定する時は、(分)の部分を”3分おき”などで試しに設定し、自動実行が行えることを事前確認するとチェックしやすいです。

翌日、再度サーバにログインし、バックアップが取れていることを確認します。

これでlolipopでバックアップの自動設定が、無料で行えるようになりました。

本当は怖い WordPress

補足:バックアップから戻すとき

ちなみにバックアップからは、下記のコマンドで戻すこと(リストア)ができます。
ただし、リストア先のDBに同名のテーブルがある場合は、上書きされてしまうので要注意です。

zcat blog_dbbackup.sql.gz  > sql.txt
mysql -u LA99999 -h mysql999.phy.lolipop.jp -pdbpassword LA99999-dbname < sql.txt


※1つ目のSQLだけ実行してsql.txtの中身を見ると、drop table/create tableがあるのが確認できます。



スッキリわかる SQL 入門 ドリル215問付き!

tarコマンドで”Exiting with failure status due to previous errors”エラー

Lolipopのレンタルサーバで稼動しているWordPressのデータをtarバックアップしていたのですが、下記のエラーが出力されました。

Exiting with failure status due to previous errors



上記エラーが出る場合、以下のような可能性が考えられます。

tarコマンドのオプションを間違えている

tarコマンドにおける-fオプションは、直後にバックアップファイル名を指定する必要があります。
-fオプションの後に、別のオプションを指定するとエラーになる場合があります。

ありがちなパターンは、gzip圧縮させるzオプションとの組み合わせでのミスです。
下記のオプションでエラーが出る場合は…

tar -cvfz backup.tgz ./backup_dir



以下のように変更するとうまくいくかもしれません

tar -cvzf backup.tgz ./backup_dir



前者側のオプション、”-cvfz backup.tgz ./backup_dir”だと、ファイルbackup.tgzと./backup_dirを、zというファイル名でバックアップする指定になってしまいます。


ファイルの権限(パーミッション)が問題の場合

バックアップ対象に、パーミッションが000のファイルがあるときにこのエラーが出力されます。
これは、バックアップ対象のファイルをtarファイルが読み取れないためです。

対象のファイルは、findコマンドに-permオプションをつけると確認できます。
今回は、WordPressのディレクトリ内にあるinstall.phpファイルがこの状態になっていました。

find ~/web -perm 000
/home/users/xxx/web/site_name/wp-admin/install.php
 
ls -l /home/users/xxx/web/site_name/wp-admin/install.php
---------- 1 foo foo 10123 Nov 19  2016 install.php



下記のコマンドで、名前を変更した上でパーミッションを一時的に変更し、サイドtarの実行でバックアップが完了できました。

mv install.php install.php_dist
chmod 400 install.php_dist
 
tar -zcvf ~/backup.tgz ./web/site_name/*


[AWS]ログイン時に使用するメールアドレスの変更方法



AWSを利用するとき、サーバの管理は通常、Web管理画面のマネジメントコンソール作業します。
このマネジメントコンソールへはメールアドレスとパスワードでログインしますが、メールアドレスを変えたい場合は下記の手順で変更できます。

Amazon Web Services実践入門

作業手順

AWSのマネジメントコンソールにログインします。
右上のアカウント名をクリック→アカウントをクリックします。



アカウント設定の欄にある編集をクリックします。



現在のログインEmailアドレスが表示されるので、右横にあるEditをクリックします。



新しいメールアドレスと、パスワードを入力します。
ここでSave Changesをクリックした時点で変更が確定します。

Use the form below to change the e-mail address for your Amazon.com account. 
Use the new address next time you log in or place an order.





次の画面で、Successが出ればOKです。


このとき、変更前の旧アドレスと変更後アドレスの両方に、下記のメールが送信されます。

Thanks for visiting Amazon.com! Per your request, we have changed the e-mail address associated with your account

The e-mail address associated with your account has been changed. The old address was old_address@example.com. The new address is new_address@example.com.
Visit Your Account at Amazon.com to view your orders, make changes to any order that hasn’t yet entered the shipping process, update your subscriptions, and much more.


これで、ログインに使用するメールアドレスの変更は完了です。


注意点

AWSで使用するアカウントは、amazon.com(AWSでは無く、ネットショップの方のアマゾン)と共有しています。ですので、本手順でアドレスを変更した場合はamazon.comへのログインIDも変わってしまいます。変更が掛かるのはUSのamazon.comのみで、日本のamazon.co.jpのアカウントは関係ないです。

変更後のアドレスの入力時に入力確認が無いため、アドレスの入力間違いには細心の注意を払ってください。

AWSに初めてアカウントを作成する時には電話のコールバックによる認証がありますが、アドレス変更時は電話は掛かってこないです。

Amazon Web Services実践入門


他のレジストラで取得したドメインをお名前.comに移管する

お名前.comなどのドメイン管理サービスでは、他のサイト(レジストラ)で取得したドメインを別のレジストラに移管することができます。

他のレジストラで登録しているドメインを、お名前.comへ移管したので手順を書いておきます。


移管するドメインの登録情報を確認する

お名前.comでは下記のドメインは移管できないため、ドメインの登録情報を事前に確認します。
条件を満たしてない場合は、あらかじめ修正しておく必要があります。

・登録期限日が間近、または登録期限日を経過している
 
・Whois情報の代行サービスを受けている場合
 
・現在のWhois情報が虚偽情報と思われる内容の場合
 
・登録者名もしくは公開連絡窓口情報が異なる複数のドメインの場合、
 一番上に入力したドメインと情報が異なるドメインは不受理(汎用JPドメインの場合)
 
・登録担当者情報にe-mailが設定されていない(属性型JPドメイン)
・技術担当者情報が複数設定されている(属性型JPドメイン)
 
・クレジットカード情報が無効の場合、更新手続きができずドメイン廃止となる場合がある



特に、Whois代行をあらかじめ解除しておく点は注意が必要です。


移管元レジストラでの転出作業

移管元レジストラ側であらかじめドメインの転出作業が必要です。
転出作業が完了すると、AuthCodeというものが発行されるので、控えておきます。

ちなみに今回は、NetworkSolutionsというレジストラから移管しました。
このときの手順は下記の記事にまとめてあります。
NetworkSolutionsからドメインを移管する(画像アリ)


お名前.comでのドメイン転入作業

まず、お名前.comのサイトにログインします。
ドメイン設定メニューより、”お名前.comへの移管申請”をクリックします。


移転したいドメイン名を入力し、次へ進むをクリックします。


移転開始の確認画面が表示されます。
表示されているドメイン名、お名前IDを確認し、AuthCodeを入手します。
(キャプチャに失敗してしまいましたが、ドメイン名が表示されている表の右に入力欄があります)



リンクにある、Whois情報を確認し、自分のメールアドレスが表示されていることを確認します。

このアドレスが自分のものでない場合は手続きが完了しないので注意してください。
メールアドレスは複数表示されますが、ドメイン移管の連絡が来るアドレスは下記のとおりです。

・汎用JPドメインの場合・・・公開連絡窓口情報[Email]
・属性型JPドメインの場合・・・登録担当者情報[電子メイル]
・上記以外のドメインの場合・・・管理担当者[Admin]メールアドレス



アドレスがOKなら、次に進むをクリックします。


ドメイン移管にはトランスファー費用が掛かりますので、決済方法を選択します。
ここで、レンタルサーバの申し込み依頼が表示されます。
サーバを持っていない場合は、ここで契約してしまっても良いですが、すでにサーバがある場合は、”いいえ”でOKです。


画面下のほうにスクロールし、決済方法を入力します。
OKなら次に進むをクリックします。


最終確認画面です。
内容を確認して…


ページ下にある、申し込むをクリックします。


完了画面が出れば、申請作業は完了です。


ドメイン転入申請後に届くメール


申請が完了すると、お名前.comの登録アドレス宛に下記のようなメールが来ます。

Subject:[お名前.com]トランスファー 受付通知
 
───────────────────────────────────
■ドメイン移管受付通知■
───────────────────────────────────
ドメイン名..................:XXXXXXXXXXXXXXXXXX
───────────────────────────────────
お名前.comをご利用いただき、まことにありがとうございます。
 
上記ドメイン名のドメイン移管申請を保留状態で受付いたしました。
弊社にて申請内容のご確認を行いますので今しばらくお待ちください。
 
※以下に該当するドメインは弊社審査において不受理とさせていただく場合が
  ございますので、あらかじめご了承ください。
 
・登録期限日が間近または登録期限日を経過している場合
・現登録業者にてWhois情報を代行するサービスを受けている場合
・現在のWhois情報が虚偽情報と思われる内容の場合(汎用JPドメイン以外)
・登録者名もしくは公開連絡窓口情報が異なる複数のドメインをお申込みいた
 だいた場合、一番上にご入力いただいたドメインと情報が異なるドメインを不受
 理とさせていただきます(汎用JPドメイン)
・登録担当者情報に[電子メイル]が設定されていない場合(属性型JPドメイン)
・技術担当者情報が複数設定されている場合(属性型JPドメイン)
 
詳細につきましては下記URLをご参照ください。
http://help.onamae.com/app/answers/detail/a_id/8590/
 
ご申請を受理させていただいた場合には、下記件名にてお名前.comへの
ドメイン移管に伴う承認手続きのメールを送信いたします。
メール本文中に承認手続きを行うURLが記載されておりますので、期日ま
でに承認手続きを行ってくださいますようお願いいたします。
 
件名:【重要】トランスファー承認手続きのお願い 
送信先:
・汎用JPドメインの場合・・・公開連絡窓口情報[Email]
・属性型JPドメインの場合・・・登録担当者情報[電子メイル]
・上記以外のドメインの場合・・・管理担当者[Admin]メールアドレス
 
※汎用JPドメインの移転手続きの場合につきましては、送信させていただくメールの
件名が下記の通りとなります。
件名:【重要】指定事業者変更(移転)承認手続きのお願い
※お名前.comでは.jpドメインの指定事業者変更・移転を含めて「ドメイン移管」
という呼称を用いております。



さらに、直後に受付開始のメールも届きます。

Subject: [お名前.com]自動更新 受付通知
───────────────────────────────────
■自動更新受付通知■
───────────────────────────────────
ドメイン名..................:XXXXXXXXXXXXXXXXXX
───────────────────────────────────
お名前.comをご利用いただき、まことにありがとうございます。
 
上記ドメイン名の自動更新申請を保留にて受付いたしました。
処理結果、または必要なお知らせを改めてメールにて通知いたしますので
今しばらくお待ちください。
※本メールにてドメイン更新の完了を保証するものではありません。
 
───────────────────────────────────
■ご注意事項■
───────────────────────────────────
・ドメインが廃止されたことで発生する損害等につきまして、弊社は一切の責
  を負わないものとさせていただきますのでご了承ください。
・更新手続き完了後のキャンセル、変更は一切できませんので、
 あらかじめご了承ください。
・クレジットカード情報が無効の場合、更新手続きができないため
 ドメイン廃止となる場合がございますので、あらかじめご注意ください。
・更新年数は該当ドメインの最低更新年数となります。
 詳細については下記ページよりご確認ください。
 http://www.onamae.com/service/d-renew/price.html?disp=1#renew-p
・登録期限日15日前よりドメイン廃止までの期間につきましては、
 自動更新設定/解除はできません。
・直近に手動にて更新手続きを完了され、行き違いで本案内が送信された場合は、
  ご容赦いただきますようお願いいたします。
・本メール記載の更新料金につきましては現時点での更新料金となります。
 実際の更新手続きはドメイン登録期限日15日前に行われ、
 更新手続き時点での料金をご請求させていただきます。
 
───────────────────────────────────
■Whois情報公開代行サービスは自動延長です■
───────────────────────────────────
Whois情報公開代行が有効のまま自動更新を行うと、Whois情報公開代行期間
も延長されます。(更新料金+Whois情報公開代行料金の請求となります。)
Whois情報公開代行期間の延長を望まない場合は、お手数ですがドメインNavi
にてドメイン登録期限16日前までにWhois情報公開代行を解除をお願いいたします。
 
───────────────────────────────────
■SSLサーバー証明書の更新■
───────────────────────────────────
お名前.com × Global Sign SSLサーバー証明書の更新は終了日の90日前より
受付しております。SSLサーバー証明書は自動更新されませんので、引き続き
ご利用の場合は、ドメインNaviからSSLサーバー証明書更新のお手続きを
行ってくださいますようお願いいたします。
 
※ドメインの登録期限日とSSLサーバー証明書の終了日は異なりますのでご注意ください。
※SSLサーバー証明書の終了日につきましてはドメインNaviよりご確認ください。
 
───────────────────────────────────
■更新しない場合はサーバー解約をお忘れなく■
───────────────────────────────────
各種サーバーサービスをご利用中でドメイン更新をされない場合は、お手数
ですが別途サーバーサービスのご解約をお願いいたします。
サーバーサービスの解約を忘れるとご請求が発生し続けます。各種サーバー
サービスのご解約に関しましては、ご利用のサーバー窓口にご連絡ください。
 
※ドメインは登録期限日を過ぎると自動的に廃止されます。
※「お名前.com×GlobalSign SSLサーバー証明書サービス」および
「お名前.com転送Plus」はドメイン廃止と同時に解約となります。
 
───────────────────────────────────
■シングルサービスご利用のお客様■
───────────────────────────────────
シングルサービスはドメインの登録期限日を経過するとサービスが停止します。
サービスの再開をご希望の場合にはドメイン復旧お手続き及び、アカウント
マネージャーCOM Navi(https://www.onamae-server.com/navi/single.html)
より有効なクレジットカード情報をご入力ください。
※クレジットカード情報入力後、1営業日程でサービスが再開いたします。 
※復旧不可のドメインの場合はドメイン解放後の再登録をご検討ください。
 http://help.onamae.com/app/answers/list/c/1%2C92%2C210/
※シングルサービスとは「ドメイン転送サービス」「ホームページサービス」
 「メールサービス」「日本語ドメイン変換転送サービス」「日本語ドメイン
 ホームページサービス」の総称です。
 
ご不明点がございましたら下記お問い合わせ先までご連絡ください。
 
今後ともお名前.comをよろしくお願いいたします。



通常は、ドメイン移管手続きをしてから、入金確認後、1~2週間前後で手続きが完了します。
メールの内容どおり、「【重要】トランスファー承認手続きのお願い 」の件名で連絡が来るので、しばらく待ちます。




ThinkPad Xシリーズの重量比較

ThinkPadの新機種(X1,X260)が出たので、買い替えの検討をかねて、歴代ThinkPad Xシリーズの重量比較をしてみました。

the比較(http://thehikaku.net/)のデータを元にしてます。
SSDにするか否かなど、構成によって若干変わるので数値は参考程度です。

ThinkPad X201s 6セルバッテリ1326g
ThinkPad X220 6セルバッテリ1523g
ThinkPad X230 4セルバッテリ1381g
ThinkPad X230 6セルバッテリ1513g
ThinkPad X240 フロント+4セルバッテリ1440g
ThinkPad X240 フロント+6セルバッテリ1592g
ThinkPad X240s1271g
ThinkPad X250 3セルバッテリ1250g
ThinkPad X250 フロント+3セルバッテリ1387g
ThinkPad X250 フロント+6セルバッテリ1535g
ThinkPad X260 3セルバッテリ1297g
ThinkPad X260 フロント+3セルバッテリ1427g
ThinkPad X260 フロント+6セルバッテリ1577g
ThinkPad X1 Carbon(2015)1307g
ThinkPad X1 Carbon(2016)約1.2kg

x201以降だと、X250のフロントバッテリー無し+3セルリアバッテリーが最軽量ぽいです。
ただ、3セルだと通常仕様で2.5Hr程度の稼動なので、持ち運びメインだと微妙かもしれません。

メールでKindleに送信したファイルの削除方法

Kindleでは、amazonで購入した書籍以外に、メールでpdfファイルなどを送付することでKindle上で読むことができるようになります。

メールで送信したファイルのことをパーソナル・ドキュメントと呼ぶのですが、このファイル、登録は簡単なのですが削除方法がちょっと分かりづらいです。

いろいろ調べてみたところ、アマゾンのWebサイト上から下記の手順を取ることで削除できました。
本当はKindle上から削除したいのですが方法が分からない…

削除方法


amazonにログインして、アカウントサービスをクリックします。


デジタルコンテンツのメニューにある、”コンテンツと端末の管理”をクリックします。
Cloud Driveの管理ではないので注意してください。


コンテンツ端末の管理メニューが表示されます。
コンテンツタブにある表示より、パーソナルドキュメントを選択します。


削除したいファイルにチェックを入れて、削除ボタンをクリックします。


確認ダイアログが表示されるので、削除しますをクリックします。
しばらく待つと、kindle端末からもファイルが削除されます。


削除したファイルを復活させることはできないので、後で読みたいファイルを一時的に削除する場合は元のファイルが残っていることを確認してください。

今、kindleの端末を購入すると、クーポンコードに「PRIMEPRICE」を入れることで、なんと4000円分のクーポンがもらえるらしいです。
「Kindle」「Kindle Paperwhite」 4,000円OFF
以下のモデルなら実質5,000円以下で手に入れることができます。
kindleのe-inkタイプなら目も疲れないので、本好きなら購入を検討するのも良いかもしれません。

Kindle Wi-Fi、ブラック、キャンペーン情報つきモデル、電子書籍リーダー
他に、Kindleの月替わりセールでは、毎月人気の作品を大幅値引き(モノによっては7割引以上も!?)しているので、こちらも要チェックです。
Kindle月替わりセール タイトル一覧

O’Reillyの書籍を買って,ワンコインでePubゲットしてみた

ずいぶん古い記事ですが、”O’reilly の書籍を買ったら,ワンコインでePubゲットできる件”という記事を見つけました。

O’reilly の書籍を買ったら,ワンコインでePubゲットできる件
http://blog.livedoor.jp/ipats/archives/25046503.html

興味があったので試してみたら、ほんとにワンコイン($4.99)で購入できました!!



せっかくなのでどんな感じで購入できるのか、アカウントを作成する部分から手順を書いておきます。


oreilly.comでアカウントを作る

まずはアカウント作成です。
oreilly.comのサイトにアクセスし、右上のYour Accountをクリックします。


Create Accountをクリックします。


メール、名前、パスワードを入力し、Create Accountをクリックします。


これでアカウントの作成は完了です。


購入済みの紙の書籍をサイトに登録する

次に自分が購入した本をサイトに登録します。

ログイン後のページに入り、メニューにあるRegister Print Booksをクリックします。


購入した本の裏にISBNが記載されているので、入力欄に登録します。


洋書の場合は、そのまま記載のISBNを入れればOKです。
日本語版の和書を持っている場合は、サイトで英語版書籍のISBNを調べた上で入力すれば良いらしいです。

入力は、ISBN10とISBN13のどちらでもOKです。また、ハイフンも入れても入れなくてもOKです。



Searchをクリックすると該当の書籍が表示されるので、Registerで登録します。


メニューより、Your Product -> Bookを選択すると、書籍が登録されているはずです。


登録した書籍のEbook版をワンコインで購入する

次は、実際にワンコイン($4.99)でEbook版を購入します。

前述のBook一覧より書籍タイトルをクリックすると明細が表示されます。
この中にある$4.99 Ebook Upgradeをクリックします。


$4.99の表示が出ない場合は…

ここで登録した書籍の新版が出ている場合、$4.99での購入ではなく、Discountの欄に新版50%オフ購入のリンクが表示されます。


この場合に、新版ではなく持っている書籍を$4.99で買いたい場合は、Ebooksのタブから選択してください。もちろん、$4.99で旧版を購入した後で新版のEbookを半額で買うこともできます。


カート内の価格が$4.99になっているのを確認し、CheckOutをクリックします。


購入ページです。
クレジットカードで購入する場合は住所などを入れる必要があるようなのですが、PayPalだと入力しなくても購入できます。今回はPayPalで購入してみたいので、”Pay with PayPal”を選択します(画像クリックで拡大します)。


PalPay側のページに遷移するので決済を完了させます。

※キャプチャを途中で失敗してしまい改めて別の書籍の購入を行ったため、これ以降の画像が別の書籍名になってしまってますが、もちろんカートに入れた商品が購入できます…

決済完了ページに遷移するので、Submit Orderをクリックします。


このページが出れば購入は完了です。



購入したEbookをダウンロードする

次に、購入したEbookをダウンロードしてみます。

一覧よりEbooksのタブを選択すると、購入したEbookが表示されます。
pdfをクリックすると、購入した書籍をダウンロードできます。


一度購入すればダウンロードは何回でも行えます。
kindleで読みたい場合は、mobi形式が読みやすいかもしれません。


Ebookを閲覧する


普通のpdfファイルですので、PC上で開けば閲覧できます。


ちょっと速度が遅いですが、全文検索もできます。


DRMフリーなので、印刷や本文内容のコピペも行えます。他のサイトで、オライリーのEbookについて調べてみると、”pdfの各ページフッタに購入者のメールアドレスが記載される”との説明があったりしますが、2011年にDRMフリーされてからはメールアドレスの記載も無いのですっきりしてます。


次に、Android版のkindleにも登録してみました。
まずはmobi形式のファイルを閲覧してみます。
こんな感じでちゃんと読めるので、通勤中に読みたい場合は良いかもしれません。


プログラムのソースコードはさすがにちょっと見づらいです。


横向きにすれば途中での改行はなくなるのですが、1ページの表示行数が短いのでイマイチですね…



一方で、pdf形式のファイルをkindleへ転送するとこのような感じになります。
紙の書籍と同じ体裁になるので、ソースコードも読みやすいです。

ですが、印刷用のレイアウトなので余白が大きすぎ、文字が小さくなってしまうのがデメリットです。左右に余白があるにもかかわらず、アスペクト比が違うので上下に黒帯が出ているのがイマイチすぎます…

この辺はリーダーの問題でもあるので、kindleでは無く余白を自動カットしてくれるpdfリーダーを使えば改善可能かとおもいます。

まとめ

以上、オライリーのサイトでEbookをワンコインで実際に購入してみました。

和書での購入ができないのは残念ですが、技術系の書籍は平易な英語で書かれているので、英語が苦手な方でも慣れれば意外と大丈夫です。洋書版のEbookを購入すると日本語と英語の対比ができるので、英語の勉強にもなるかもしれません。

また、登録のプロセスを見てもらうと分かるのですが、購入済書籍の登録ではISBNを入れるだけです。ISBNはサイトで調べれば書籍を買わなくても調べることが可能なので、本当に書籍を買ったかの確認は取らず、ユーザの入力情報を信じる性善説な仕組みです(pdfにDRMが掛かってないのも性善説に基づいてます)。

ですので、買ってない本も登録して5$で購入できてしまうのですが、利用規約にもあるよう虚偽の情報を入力した場合はアカウントの停止だけでなく・将来にわたってオライリーのサービスが利用できないことにもなってしまうので、行ってはいけません(注意するまでも無く当たり前のことですが)。

REGISTRATION DATA
If we have reason to believe that you have failed to do so, we may suspend or terminate your account, and refuse or terminate any current or future use of the site or any O’Reilly service.



1冊辺りの価格も安いので、オライリー好きの方は一度試してみてはいかがでしょうか?
オライリーの最新書籍をチェック

トラックポイントの”センターボタン+スクロール問題”まとめ(tp4table.dat)

ThinkPadと同等の機能を持つキーボードをデスクトップPCやスマホで利用できる、”ThinkPadトラックポイント・キーボード”という製品を、レノボが販売しています。

レノボ・ジャパン ThinkPad Bluetooth ワイヤレス・トラックポイント・キーボード

このキーボード、ThinkPadユーザには便利なのですが、実際のThinkPadで使うときと比べて、センターボタン+スクロールの振る舞いが、使用するアプリによって変わってしまう問題があります。
これについて、調べたので備忘録代わりに残しておきます。

はじめに

トラックポイントの振る舞いですが、ドライバをインストールするとtp4table.datというファイルが作られ、この中にセンターボタン+スクロール処理に関する定義が存在しています。

ファイルは、製品・ドライバによって異なるかもしれませんが、下記の場所辺りにあるはずです。
c:\Program Files\Lenovo\TrackPoint\tp4table.dat
c:\Program Files\Synaptics\SynTP\TP4table.dat
c:\Program Files\system32\tp4table.dat


また、センターボタン+ドラッグの振る舞いは、コントロールパネルでも一部設定できます。ですので、まずコントロールパネルのマウス設定からを変えてみると、思ったとおりに操作できるかもしれません。

tp4table.datの設定例

コントロールパネルの設定でうまくいかない場合は、tp4table.datの定義を書き換えます。
具体的には、特定のアプリ・入力エリア・操作に対して定義を書くのですが、自力で調べるのは難しいので、他のページで見かけた記入例を記載しておきます(出典をコメントに書いてあります)。

; Firefox
; http://web.t-factory.jp/nicotech/?p=148
*,*,firefox.exe,*,*,*,WheelStd,0,9
 
; firefox
; http://wriver.blogspot.com/2010/01/firefoxtrackpoint.html
*,*,firefox.exe,*,*,MozillaWindowClass,WheelVkey,0,9
 
; chrome
; http://blog.clock-up.jp/entry/2015/02/26/thinkpad-trackpoint-scroll-chrome
*,*,chrome.exe,*,*,Chrome_WidgetWin_1,WheelVkey,0,9
 
; Visual Studio 2010
; http://nx9420.blogspot.jp/2009/12/how-to-fix-ibm-lenovo-trackpoint-scroll.html
*,*,VWDExpress.exe,*,*,*,WheelStd,0,9
 
; for Visual Studio 2010
; http://winusb.at.webry.info/201011/article_1.html
*,*,devenv.exe,*,*,*,WheelStd,0,9
 
 
; VMWare
; http://isnism.com/log/2013-01-24-ID1550.html
*,*,vmware.exe,*,*,MKSEmbedded,SendInput,0,9
 
; VMWare-VMX
*,*,vmware-vmx.exe,*,*,MKSEmbedded,SendInput,0,9
 
; VMware Player
*,*,vmplayer.exe,*,*,*,WheelStd,0,9 
 
 
; gvim support
; http://vim.wikia.com/wiki/Fix_scrolling_when_using_Trackpoint
*,*,gvim.exe,*,*,*,WheelStd,0,9


※上記以外に紹介されている定義がありましたら、コメント欄に書いていただければ追記しておきます。

VMWareの場合は、さらに下記の定義を書くと良いらしいです。

[AutoScrollTable]
*,*,vmware.exe,*,smooth
*,*,vmware-vmx.exe,*,smooth
*,*,vmplayer.exe,*,smooth




ファイルを保存したら、PCを再起動させます。
試行錯誤している最中は、下記のプログラムをタスクマネージャから終了させて再起動させても良いです。
c:\Program Files\Lenovo\TrackPoint\tp4serv.exe
c:\Program Files\Synaptics\SynTP\SynTPEnh.exe


オマケ:スクロールカーソルをOffにさせる

センターボタン+スクロールがの振る舞いの問題とは別に、マウスカーソルの表示がおかしくなる症状が発生する場合があります。センターボタン+スクロールの操作中は、スクロール中が分かるものにマウスカーソルが変化するのですが、これが操作後に戻らない場合があります。

このような場合は、下記のレジストリを書き換えてSynTPEnh.exe を再起動させるとうまくいくかもしれません。
レジストリキーが無い場合は追加します。
HKEY_LOCAL_MACHINE\SOFTWARE\Synaptics\SynTPEnh
DWORD値 UseScrollCursor=0


面倒な場合は、以下のファイルを、changeScrollCursor.regとして保存し、ファイルをダブルクリックすればOKです。

Windows Registry Editor Version 5.00
 
[HKEY_LOCAL_MACHINE\SOFTWARE\Synaptics\SynTPEnh]
"UseScrollCursor"=dword:00000000


http://forums.laptopvideo2go.com/topic/15103-latest-synaptics-touchpad-drivers/page-9より


いろいろ面倒ですが、一度設定してしまえば快適な操作環境が手に入るのでぜひ試行錯誤してみてください。



このシリーズ、現時点では下記の4つが販売されています。
この中でも、Bluetooth版はスリープ後の起動時にすぐ入力できないので、自宅で使うならUSB版がお勧めです。
(出先でスマホにつなげたい場合は、Bluetooth版でもOKかもしれません)
0B47208: USB接続 JIS配列

0B47190: USB接続 US配列

0B47181: Bluetooth接続 JIS配列

0B47181: Bluetooth接続 US配列

C#でReactive Extensions(Rx)その1:まずはIObserver, IObservableを理解する

今回より、C#でRxの使い方を確認していきます。

Rxはデザインパターンの1つであるのObserverパターンを元にしています。
なのでRxを学ぶ前に、まずはC#でObserverパターンを表現するためのIObserver, IObservableクラスの使い方を確認します。

今回作るのは以下のようなコードです。
ここに出てくる、SimpleEventSource/PrintEventListenerクラスを作るのが今回のゴールです。

public void Sample01() {
    // イベントソースを作る
    SimpleEventSource source = new SimpleEventSource();
 
    // リスナを登録する
    source.Subscribe(new PrintEventListener("A"));
 
  // 処理をexecuteする。(この時、PrintEventListenerへイベントが通知される)
    source.Execute(1);
    source.Execute(2);
    source.Execute(3);
}


まず、今回出てくる2つのクラスの概要を説明します。
SimpleObserverは、Execute()メソッドでなんらかの処理を行うクラスです。
このクラスは処理を実行するだけでなく、Execute()が実行時に登録されているイベントリスナに対して、Execute実行がされたことをイベントとして通知します。

イベントリスナになっているのが、PrintEventListenerクラスです。
イベントリスナはSimpleEventSource::Subscribe()を使って登録します。

ここで、イベントソースであるSimpleEventSourceがIObservableを実装し、PrintEventListenerがIObserverを実装します。

class PrintEventListener : IObserver<int> {
    ...
}
 
class SimpleEventSource : IObservable<int> {
    ...
}


IObserverとIObservableの2つは、.Net Frameworkが用意しているインターフェースです。


IObserverの実装(イベントリスナ)


まずは、処理内容が分かりやすいイベントリスナ側を実装します。
ちなみに、イベントリスナは、Rx上ではサブスクライバ(購読者)と呼ばれています。

// サブスクライバの実装(いわゆるイベントリスナ)
class PrintEventListener : IObserver<int> {
    private string name;
    public PrintEventListener ( string name ) {
        // 区別するための名前を記録(IObserverの実装とは関係のないDebug用)
        this.name = name;
    }
 
    // イベント受信時のハンドラ
    public void OnNext ( int value ) {
        Console.WriteLine( name + ":イベントを受信 val="+value );
    }
 
    // イベント通知終了時のハンドラ(イベントソースから切られた)
    public void OnCompleted () {
        Console.WriteLine( name + ":イベントが終了しました" );
    }
 
    // エラー時のハンドラ
    public void OnError ( Exception error ) {
        Console.WriteLine( name + ":イベント受信中にエラー発生 Msg=" + error.Message );
    }
}


この中で最も大事なのは、OnNext()です。
OnNext()は、イベントが発火したとき、コールされるコールバック(イベントハンドラ)です。
IObserverインターフェースでは、このほかにイベントが最後になった事とエラーになった事を伝えるために、OnCompleted()・OnError()の実装を要求されています。

イベントが最後というのは、たとえば通信プログラムを作っている際、通信が切断されるときなど、イベントソース側からこれ以上イベントが来なくなる事を通知するハンドラです。

エラーになった事というのは文字通り、想定外のエラーが発生しイベントの実行が行えなくなった事で、このときにOnError()をコールされることでエラー通知されます。


IObservableの実装(イベントソース)

次に、イベントを発火させる側の処理です。
イベントソースは、Rxではパブリッシャ(出版者)と呼ばれます。

イベントソース側の処理は長いので、ちょっとづつ見ていきます。
まずはリスナの登録処理である、Subscribe()です。

// オブザーバの実装(いわゆるイベントソース)
class SimpleEventSource : IObservable<int> {
    // イベントリスナの一覧
    private List<IObserver<int>> observerList = new List<IObserver<int>>();
 
    // イベントリスナの登録
    public IDisposable Subscribe( IObserver<int> observer ) {
        observerList.Add(observer);
        return new NotifyDispose(observerList, observer);
    }


Subscribeでは、IObserverのオブジェクトを受け取ることでリスナを登録します。
複数のリスナを登録可能にするため、今回はList<>でリスなの一覧を登録質得ます。

また、リスナの削除を行うためのDisposerオブジェクトを渡しています(詳細は後述)。


次に、イベントの実行処理です。
このメソッド名は何でも良いですが、今回はExecute()にしました。

    // 処理の実行(実行されたことのOnExecute通知を行う)
    public void Execute ( int value ) {
        // do something...
    
        if( value < 0 ) {
            foreach( var ob in observerList ) {
                ob.OnError(new Exception("invalid value:" + value));
            }
            observerList.Clear();
            return;
        }
 
        if( value > 10 ) {
            foreach( var ob in observerList ) {
                ob.OnCompleted();
            }
            observerList.Clear();
            return;
        }
 
        // イベントを通知する
        foreach( var ob in observerList ) {
            ob.OnNext(value);
        }
    }


今回のサンプルでは、負の数が入力されたらエラー、10以上の数値が入力されたら終了。ということにしてみました(この処理が何であるかは、パブリッシャの実装に依存します)。

IObservableインターフェースを実装するものは、エラーが起きた時はOnError()、イベントの終了はOnCompleted()を呼ぶのが決まりとなっています。このためExecute()ではこのルールどおりそれぞれのメソッドを実行しています。
また、IObserverではOnError()およびOnCompleted()を読んだ後は、OnNext()をコールしてはいけないというルールもある為、observerList.Clear();でリスナを削除しています。


最後に、前述のSubscribe()で戻り値としてリターンしていたDisposeオブジェクトです。
今回は、SimpleEventSourceのインナークラスとして実装しています。

    // Listenerの削除管理クラス
    private class NotifyDispose : IDisposable {
        private List<IObserver<int>> observerListRef = null; // SimpleObserverが持つリスナ一覧への参照
        private IObserver<int>       targetObserver  = null;
 
        // コンストラクタ
        public NotifyDispose ( List<IObserver<int>> observerListRef, IObserver<int> targetObserver ) {
            this.observerListRef = observerListRef;
            this.targetObserver  = targetObserver;
        }
 
        // 削除処理
        public void Dispose () {
            if( this.observerListRef == null ) {
                // 既に削除が終わっていたら何もしない
                return;
            }
 
            if( observerListRef.IndexOf(targetObserver) != -1 ) {
                // 監視中だったら、監視対象から外す
                observerListRef.Remove(targetObserver);
            }
 
            // 削除完了をマーキングする
            observerListRef = null;
            targetObserver  = null;
        }
    }
}


ちょっと長いですが、行っていることはシンプルです。
コンストラクタでSimpleEventSourceが持っているリスナ一覧への参照を覚えておき、Dispose()がコールされたらリスナ一覧から登録したリスナを削除しているだけです。


作ったIObserver/IObservableを連携させてみる。

最後に、作ったPrintEventListenerとSimpleEventSourceを連携させて、呼び出しテストを行います。

今回は下記のコードを書いてみました。

public void ObserverableTest () {
    // イベントソースを作る
    SimpleEventSource source = new SimpleEventSource();
 
    // リスナ登録し処理をexecuteする。(この時、OnExecuteのイベントが通知される)
    source.Subscribe(new PrintEventListener("A"));
    source.Execute(1);
    source.Execute(2);
    source.Execute(3);
    source.Execute(100);
    source.Execute(4);  // このイベントは通知されない
 
    // 2つのリスナへ通知を送る. 途中でエラーにさせる
    source.Subscribe(new PrintEventListener("B1"));
    source.Subscribe(new PrintEventListener("B2"));
    source.Execute(1);
    source.Execute(2);
    source.Execute(-10);
    source.Execute(3);  // このイベントは通知されない
 
 
    // 呼び元主導で、Listenを中断してもらう。
    IDisposable disposerC1 = source.Subscribe(new PrintEventListener("C1"));
    IDisposable disposerC2 = source.Subscribe(new PrintEventListener("C2"));
    source.Execute(1);
    source.Execute(2);
 
    disposerC1.Dispose();
    source.Execute(3);   // このイベントはC1には通知されない
 
    disposerC2.Dispose();
    source.Execute(4);   // このイベントは通知されない
}


いろいろなリスナ登録・削除の処理を行っています。


この処理の実行結果は以下のとおりです。

A:イベントを受信 val=1
A:イベントを受信 val=2
A:イベントを受信 val=3
A:イベントが終了しました
 
B1:イベントを受信 val=1
B2:イベントを受信 val=1
B1:イベントを受信 val=2
B2:イベントを受信 val=2
B1:イベント受信中にエラー発生 Msg=invalid value:-10
B2:イベント受信中にエラー発生 Msg=invalid value:-10
 
C1:イベントを受信 val=1
C2:イベントを受信 val=1
C1:イベントを受信 val=2
C2:イベントを受信 val=2
C2:イベントを受信 val=3


思ったとおりの出力になっているか、確認してみてください。


今回は、以上で終了です。
.NetFrameworkが用意しているIObserverおよび、IObservableクラスを利用し、Observerパターンの実装を行いました。


[C#]WinForm環境でRxを利用するためのインストール方法

VisualStudioでWinFormを使用してプログラミングしているとき、Rx(Reactive Extension)ライブラリを使用する方法を説明します。
ライブラリのインストールは、NuGetを使って参照設定を行うのが最も簡単です。

Unityなど、ゲームエンジンからC# + Rxの組み合わせで開発したい場合も多いかと思いますが、最初からUnity上でUniRxを使って開発すると大変なので、まずはWinForm上で使い方を練習すると良いです。


それでは、作業手順です。
まず、プロジェクトを作成し、ソリューションエクスプローラの参照設定を右クリックします。
メニューにある、NuGetパッケージの管理をクリックします。



左のメニューでnuget.orgを選択し、検索欄に”rx linq”と入力します。


しばらく待つと結果が表示されるので、以下のものをインストールします。
作成者の欄がMicrosoftになっているはずです。

Reacive Extensions - Query Library
user to express complex event prosessing queries over observable sequences.






すると、依存ライブラリとしてRx-Interfaces, Rx-Coreも選択されるので、同意するをクリックします。

※実は、Rx-Core(Reacive Extensions – Core Library)だけでもRxは使えるのですが、C#でRxを使う場合は通常LinqのQuery Libaryも使うので纏めてインストールしています。


インストールが終わると、参照設定に下記の3つが入っていることを確認します。

System.Reactive.Core
System.Reactive.Interfaces
System.Reactive.Linq





インストールが完了したら動作確認です。
今回は画面にボタンを置いて、イベントハンドラに動作確認コードを書いてみます。

サンプルコードは下記の1行だけです。
処理内容は、1~10のデータをイベントとして送信し、イベントハンドラでデバッグ出力を行っています。

private void button1_Click(object sender, EventArgs e) {
    System.Reactive.Linq.Observable.Range(1, 10).Subscribe(x => System.Diagnostics.Debug.WriteLine(x));
}




プログラムを実行し、出力ウィンドウに1~10の数字が出ればOKです。


C#で2048ゲームのクローンを作る[その6]

今回は、前回に作ったのセルのアニメーション処理を追加します。


アニメーションをさせるということは、各セルに対して定期的に再描画のUpdate処理をコールしてあげる必要があります。このためUpdate処理を実装します。

最初に、Updateメソッドだけを持つインターフェースを作ります。

public interface ITaskBehaviour {
    void Update();
}



作ったインターフェースをCellScriptクラスに適用します。Updateメソッドは、一旦デバッグメッセージの出力として実装しておきます。

class CellScript : ITaskBehaviour {
    public void Update() {
        // Step1:動作確認
        System.Diagnostics.Debug.Print( "Updateが呼ばれました:value={0}, location=[{1},{2}]",
                                        lblCell.Text,
                                        lblCell.Location.X, lblCell.Location.Y );
    }
}




次に、呼び元側となる画面のForm1クラスを修正します。

    public partial class Form1 : Form {
        List<ITaskBehaviour> taskList = new List<ITaskBehaviour>();
 
        //*********************************************************************
        /// <summary> 画面表示時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_Load( object sender, EventArgs e ) {
            ...
 
            timGameTimer.Interval = 100;
            timGameTimer.Enabled = true;
 
            // 動作確認用のセルを生成する
            taskList.Add( new CellScript( this.pnlBase,   64, 0, 0 ) );
            taskList.Add( new CellScript( this.pnlBase,  256, 1, 0 ) );
            taskList.Add( new CellScript( this.pnlBase, 1024, 2, 0 ) );
            taskList.Add( new CellScript( this.pnlBase,65535, 3, 0 ) );
 
            taskList.Add( new CellScript( this.pnlBase,    8, 0, 1 ) );
            taskList.Add( new CellScript( this.pnlBase,    4, 1, 1 ) );
 
            taskList.Add( new CellScript( this.pnlBase,    2, 3, 2 ) );
 
            ...
        }
 
 
        private void timGameTimer_Tick(object sender, EventArgs e) {
            // すべてのタスク(セル)に定期的に処理を行わせる
            foreach (ITaskBehaviour task in taskList) {
                task.Update();
            }
        }
    }
}



先ほど定義したインターフェースを持つオブジェクト(セル)達を管理するtaskListメンバ変数として定義します。画面表示の初期化処理であるForm1_Load()でListにセルたちを入れ込んでいます。

画面にtimeGameTimerというTimerコントロールをおいた上で、timGameTimer_Tick()イベントをタイマー実行させます。timGameTimer_Tickが行っていることは、先ほど登録したセルたちのUpdate()メソッドを順にコールするだけです。呼び出し周期は、”timGameTimer.Interval = 100;”としているため100mSec毎となり、毎秒10フレームで表示(10FPS)されます。
ここでInterval=20など小さくすればより滑らかにアニメーション動作させることができます(ただし頻繁に書き換えるので負荷は上がります)。

ここまで書いて一旦実行させると…

出力ウィンドウにUpdateメソッドのデバッグログが出力され続けます。
(出力ウィンドウが無い時は、Alt+2キーで表示できます
これで、Update()の定期的な呼び出しができるようになりました。


次に、CellScriptのアニメーション処理を実装します。
セルの状態ととしては、静止状態(IDLE)とスライドのアニメーション中状態(SLIDE)があるので、状態の一覧であるSTATE列挙体と現在の状態を持つcurStateを定義します。

class CellScript {
 
    enum STATE {
        IDLE,
        SLIDE
    }
 
    STATE curState = STATE.IDLE;



コンストラクタでの初期化時にcurStateをIDLEに初期化します。
また、setPositionで移動先が指定されると、最終移動先をdestLocationに、現在位置とのずれをslideOffsetに入れます。
スライド直後はdestLocation + slideOffsetが現在位置になりますが、アニメーションで徐々にdestLocationの位置に移動させていきます。

SLIDE_FRAMEでは、アニメーションにかかる時間(フレーム数)を指定しています。たとえば、10FPSでSLIDE_FRAME=50だったら、5秒かけて移動のアニメーションが走ることになります。

    const int SLIDE_FRAME = 50;
 
    int slidingFrame = 0;
    Point slideOffset  = new Point(0,0);
    Point destLocation = new Point(0,0);
 
    //*********************************************************************
    /// <summary> コンストラクタ
    /// </summary>
    /// <param name="value"></param>
    /// <param name="x"></param>
    /// <param name="y"></param>
    //*********************************************************************
    public CellScript( Panel pnlBoard, int cellValue, int x, int y ) {
        this.pnlBoard = pnlBoard;
        _createLabel();
        initPosition( x, y );
        setValue( cellValue );
 
        curState = STATE.IDLE;
    }
 
    //*********************************************************************
    /// <summary> セルの初期位置を指定する
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    //*********************************************************************
    public void initPosition(int x, int y) {
        if (lblCell == null) {
            return;
        }
 
        lblCell.Location = new Point( x*CELL_SIZE + (x+1)*CELL_PADDING,
                                      y*CELL_SIZE + (y+1)*CELL_PADDING );
    }
 
    //*********************************************************************
    /// <summary> セルを移動させる
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    //*********************************************************************
    public void setPosition(int x, int y) {
        if (lblCell == null) {
            return;
        }
 
        // 移動先を求める
        destLocation = new Point( x*CELL_SIZE + (x+1)*CELL_PADDING,
                                  y*CELL_SIZE + (y+1)*CELL_PADDING );
 
        // 現在位置と、移動先の差(移動距離)を求める
        slideOffset = new Point( lblCell.Location.X - destLocation.X,
                                 lblCell.Location.Y - destLocation.Y );
 
        // 状態を移動中にする
        curState = STATE.SLIDE;
        slidingFrame = 0;
    }




次はUpdate()メソッドです。こちらは、現在の状態(curState)に応じて処理を変えます。
IDLEの時は何もしません。
SLIDEの時、SLIDE状態になってからのは経過フレーム(slidingFrame)に応じて、徐々にセルをdestLocationへ近づけていきます。そして経過フレームがSLIDE_FRAMEに達したらIDLEに戻します。

    public void Update() {
 
        switch (curState) {
            case STATE.IDLE:
                // do nothing
                break;
            case STATE.SLIDE:
                slidingFrame += 1;
 
                int x = destLocation.X + (int)( slideOffset.X * ( (SLIDE_FRAME-slidingFrame) / (float)SLIDE_FRAME ) );
                int y = destLocation.Y + (int)( slideOffset.Y * ( (SLIDE_FRAME-slidingFrame) / (float)SLIDE_FRAME ) );
                lblCell.Location = new Point( x, y );
 
                if (slidingFrame >= SLIDE_FRAME) {
                    slidingFrame = 0;
                    curState = STATE.IDLE;
                }
                break;
        }
    }


これを実行すると、セルのアニメーションが行えます。

最後にForm1にボタンを貼り付け、クリックしたら特定のセルの位置を変え(=STATEをSLIDE_FRAMEに変え)れば、アニメーションが行われます。

        private void button1_Click(object sender, EventArgs e) {
            cellScript.setPosition( 3, 3 );
        }



セルをたくさん用意し、以下のようなコードを書けば大量のセルたちを一斉にスライドさせることも簡単です。

    class Form1 {
        ...
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 移動先のセル(本当はBoardManagerが計算すべきものだが、動作確認のために手入力している)
            List<Tuple<int,int>> destPos = new List<Tuple<int,int>>();
            destPos.Add( new Tuple<int,int>(0,2) );
            destPos.Add( new Tuple<int,int>(1,2) );
            destPos.Add( new Tuple<int,int>(2,3) );
            destPos.Add( new Tuple<int,int>(3,2) );
            destPos.Add( new Tuple<int,int>(0,3) );
            destPos.Add( new Tuple<int,int>(1,3) );
            destPos.Add( new Tuple<int,int>(3,3) );
 
            // 登録されているすべてのセルを、上に書いた場所へ移動させる
            int offset = 0;
            foreach (ITaskBehaviour task in taskList) {
                CellScript cellScript = task as CellScript;
                int x = destPos[offset].Item1;
                int y = destPos[offset].Item2;
 
                cellScript.setPosition( x, y );
                offset++;
 
            }
        }
 
        ...

[PostgreSQL]COPYコマンドでデータを登録できない時にチェックすること

PostgreSQLではCOPYコマンドで複数のデータを一度に登録することができます。

コマンドラインからコピー&ペーストでCOPYコマンドを貼り付けて実行したとき、下記のエラーが出るときがあります。

ERROR: invalid input syntax for ....
CONTEXT table_name, line 1, column ....



この場合、貼り付けるときの改行コードを変更すると正しく登録できる可能性があります。
Windowsの場合は改行コードがLFのデータはロードができず、CR+LFに変更したらOKでした。

[Postgres]すべてのシーケンスを1にリセットするSQLを生成する。

下記のSQLで、すべてのシーケンスを1にリセットするSQLを生成することができます。

SELECT    'SELECT setval(''' || c.relname || '', 1, false);'
FROM      PG_CLASS AS c 
LEFT JOIN PG_USER  AS u ON c.relowner = u.usesysid 
WHERE     c.relkind = 'S'
ORDER BY  c.relname;



出力結果がSQL文になっているので、コピーして実行すればリセットできます。
特定のシーケンスだけ取得したい場合は、WHEREを工夫するか、クリアしたいテーブルだけSQL実行すればよいです。

C#で2048ゲームのクローンを作る[その5]

今回は、予定通り盤面に表示させる駒(セル)を管理するクラスを作成します。
これまでのゲームルール作成は一旦置いておき、描画寄りのプログラムになります。

今回作るプログラムで、以下のような盤面の表示を行えるようにします。



CellScriptクラスの作成(概要)

まずは、セルを管理するCellScriptクラスを作成します。
大枠だけまず書くと、以下のような感じになります。
CellScript.cs

using System.Windows.Forms;
using System.Collections.Generic;
using System.Drawing;
using System;
 
class CellScript {
    /// <summary>セルの大きさ</summary>
    public const int CELL_SIZE = 50;
 
    /// <summary>セル間の隙間</summary>
    public const int CELL_PADDING = 4;
 
    private Panel pnlBoard;
    private Label lblCell;
 
    // コンストラクタ
    public CellScript( Panel pnlBoard, int cellValue, int x, int y ) {
        ...
    }
 
    // セルの位置を指定する
    public void setPosition(int x, int y) {
        ...
    }
 
    // セルに値をセットする
    public void setValue (int cellValue) {
        ...
    }
}



クラス定義を見てみると、まず、セルの大きさや隙間のサイズです。これはいろいろな所で必要となるので、定数にしておきます。

/// <summary>セルの大きさ</summary>
public const int CELL_SIZE = 50;
 
/// <summary>セル間の隙間</summary>
public const int CELL_PADDING = 4;



次に、内部的なメンバ変数として、駒を表現するためLabelと、駒を置く盤になるPanelオブジェクトを保持します。ここで出てくるLabelは、今回GUIの描画にWinFormを使用しているため、正確に書くとSystem.Windows.Forms.Labelクラスです。Panelも同様です。

private Panel pnlBoard;
private Label lblCell;



今回作るメソッドは、コンストラクタ、位置の移動、値のセットの3つです。将来的にはセルの状態管理やアニメーションを行う為にメソッドを増やすつもりですが、最低限これだけあれば表示は可能です。

// コンストラクタ
public CellScript( Panel pnlBoard, int cellValue, int x, int y );
 
// セルの位置を指定する
public void setPosition(int x, int y);
 
// セルに値をセットする
public void setValue (int cellValue);


それでは、これらのメソッドの中身を作っていきます。


CellScriptクラスの作成(詳細)

まずは、コンストラクタからです。今回作ったソースはこちらです。

//*********************************************************************
/// <summary> コンストラクタ
/// </summary>
/// <param name="value"></param>
/// <param name="x"></param>
/// <param name="y"></param>
//*********************************************************************
public CellScript( Panel pnlBoard, int cellValue, int x, int y ) {
    this.pnlBoard = pnlBoard;
    _createLabel();
    setPosition( x, y );
    setValue( cellValue );
}


行っていることはシンプルで、GUIパーツであるラベルオブジェクトを生成し、値・位置を初期化しているだけです。


次に、コンストラクタから呼び出される_createLabel()を見てみます。

//*********************************************************************
/// <summary> セルのラベルを作成する
/// </summary>
//*********************************************************************
private void _createLabel() {
    if (pnlBoard == null) {
        return;
    }
 
    lblCell = new System.Windows.Forms.Label();
 
    lblCell.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
    lblCell.Location = new System.Drawing.Point(0, 0);
    lblCell.Name = "cellLabel";
    lblCell.Size = new System.Drawing.Size(CELL_SIZE, CELL_SIZE);
    lblCell.Text = "";
    lblCell.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
 
    pnlBoard.Controls.Add(this.lblCell);
    lblCell.BringToFront();
}


ここではWinFormのコントロールを自動生成しています。
このコードの作り方は、別の記事で説明しています。
LabelなどGUIコントロールの動的生成コードを一瞬で作る方法

メソッドの最後でpnlBoard.Controls.Add()としているので、作ったLabelはパネルの上に作成されます。


_createLabel()でlblCellにLabelオブジェクトを作成したら、次は場所の指定です。
LabelオブジェクトはLocationプロパティで場所を指定できるため、セルの大きさとセル間の隙間を考慮して、セルを置く左上の座標を求めています。

//*********************************************************************
/// <summary> セルの位置を指定する
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
//*********************************************************************
public void setPosition(int x, int y) {
    if (lblCell == null) {
        return;
    }
 
    int dispPixelOffsetX = x*CELL_SIZE + (x+1)*CELL_PADDING;
    int dispPixelOffsetY = y*CELL_SIZE + (y+1)*CELL_PADDING;
 
    lblCell.Location = new System.Drawing.Point(dispPixelOffsetX, dispPixelOffsetY);
}




次に、値をセットするsetValue()メソッドと、そこから呼ばれるprivateメソッドを一度に見てみます。
基本的には、lblCell.Textで値をセットしておしまいなのですが、値によって色を変更するのと、桁数が増えると表示が折り返されてしまうので、桁数が増えるたびにフォントサイズを小さくして折り返されないようにする処理を入れています。

//*********************************************************************
/// <summary> セルに値をセットする
/// </summary>
/// <param name="cellValue"></param>
//*********************************************************************
public void setValue (int cellValue) {
    lblCell.Text = cellValue.ToString();
 
    // セルの値に応じた色/フォントをセットする
    lblCell.BackColor = _getCellColor( cellValue );
    lblCell.Font      = _getCellFont( cellValue );
}
 
//*********************************************************************
/// <summary> セルの値に応じた色を返す
/// </summary>
/// <param name="cellValue"></param>
/// <returns></returns>
//*********************************************************************
private Color _getCellColor( int cellValue ) {
    // セルの値を元に何番目かを求める 2:1番目, 4:2番目, 8:3番目, 16:4番目 ...
    int offset = (int)Math.Log(cellValue, 2 );
    offset = Math.Max( 0, offset);
 
    // 色を求める(番号が増えるほど青暗くするが、見づらいので0x60以下には暗くしない)
        int col = Math.Max( 60, 255 - offset*20 );
    return Color.FromArgb( 255, col, col, 0xff);
}
 
//*********************************************************************
/// <summary> セルの値に応じたフォントを返す
/// </summary>
/// <param name="cellValue"></param>
/// <returns></returns>
//*********************************************************************
private Font _getCellFont( int cellValue ) {
    // 数字の大きさに合わせてフォントサイズを切り替える
    float fontSize = 12;
    if (cellValue >= 1000) {
        fontSize = 10;
    }
    if (cellValue >= 10000) {
        fontSize = 8;
    }
    return new System.Drawing.Font("Meiryo UI", fontSize, 
                                    System.Drawing.FontStyle.Regular, 
                                    System.Drawing.GraphicsUnit.Point, 
                                    ((byte)(128)));
}



上記の処理で、CellScript.csは一旦完了です。


FormからCellScript.csを呼び出す

次は、Formから先ほど作成したCellScriptを実行します。
Formにパネル(pnlBase)を貼り付けた上で、下記のコードを実行します。

public partial class Form1 : Form {
    //*********************************************************************
    /// <summary> 画面表示時のハンドラ
    /// </summary>
    //*********************************************************************
    private void Form1_Load( object sender, EventArgs e ) {
 
        // 動作確認用のセルを生成する
        new CellScript( this.pnlBase,   64, 0, 0 );
        new CellScript( this.pnlBase,  256, 1, 0 );
        new CellScript( this.pnlBase, 1024, 2, 0 );
        new CellScript( this.pnlBase,65535, 3, 0 );
 
        new CellScript( this.pnlBase,    8, 0, 1 );
        new CellScript( this.pnlBase,    4, 1, 1 );
 
        new CellScript( this.pnlBase,    2, 3, 2 );
 
 
 
        // 盤面の大きさを調整する
        int boardSize     = CellScript.CELL_SIZE    * BoardManager.BOARD_SIZE +
                            CellScript.CELL_PADDING * (BoardManager.BOARD_SIZE+1) +
                            2;
        this.pnlBase.Size = new System.Drawing.Size(boardSize, boardSize);
    }
}



このコードを実行すると、最初にあった画像のような盤面が表示されます。
想定したとおり、数字によって色が変わっていたり、桁数によって文字の大きさが変わっていれば成功です。


これで基本的なセルの描画は完了しました。
次回も引き続きCellScriptを改良し、作ったセルを移動させるアニメーション処理を追加する予定です。

[C#]System.Drawing.ColorのListを初期化する方法

C#で、Colorクラスを要素に持つListをまとめて初期化したい場合があります。このような場合、下記のコードで宣言と初期化を一度に行えます。
Colorクラスはコンストラクタで色指定できないので、FromArgb()などのメソッドを使ってインスタンス生成します。

using System.Drawing;
 
public class SomeClass {
    public List<Color> colorList = new List<Color>{ 
                                        Color.FromArgb(255, 255,255,0),
                                        Color.Red,
                                        Color.Blue,
                                        Color.Yellow };
}



定数にしたい場合は、constをつけておけばOKです。

public List<Color> ...public const List<Color> ...




Listで無く配列を初期化したい時は、以下のような感じです。

using System.Drawing;
 
// 一旦Colorの配列を作る
Color[] colorArray = { Color.Red, 
                       Color.Blue,
                       Color.FromArgb(255,255,128,128),};





また、ちょっと話がずれますが、あらかじめ用意されたColorの配列を、Listに一括代入したい時はAddRange()を使用します。

using System.Drawing;
 
// 一旦Colorの配列を作る
Color[] colorArray = { Color.Red, 
                       Color.Blue,
                       Color.FromArgb(255,255,128,128),};
 
// List<Color>を、あらかじめ定義されたColorの配列で初期化
List<Color> colorList = new List<Color>();
colorList.AddRange(colorArray);



上記のコードをもう少し端折ると、以下のような感じになります。

// List<Color>を、ワークの配列変数を用意せず初期化
List<Color> colorList2 = new List<Color>();
colorList2.AddRange( new Color[] { 
                        Color.Red, 
                        Color.Blue, });



基礎からきちんと知りたい人の C#プログラミング入門

[DMM Mobile]WebAPI仕様:利用可能な高速通信量の確認

DMMのNVMO SIMに対する、利用可能な高速通信データ残量や追加容量チャージのクーポン情報取得に関するWebAPIの仕様です。


Mvno_List.GetCoupon:クーポン一覧問合せ


Resuest:

使用している端末がAndroidの場合は、URLに”method=AndroidApp”、POSTパラメータのappidにandroid_mvnoをセットする。messageに要求APIを指定する。

POST https://www.dmm.com/service/-/json/=/method=AndroidApp HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: www.dmm.com
Connection: Keep-Alive
Accept-Encoding: gzip
 
[POST Params]
authkey %AUTH_KEY%
appid   android_mvno
message Mvno_List.GetCoupon
params  {}




Response:

応答データは、JSON形式で返される
data/coupon_info/data_priceは税抜き金額、data_price_fullには税込金額がセットされる。
data/coupon_info/is_onetimeがfalseのレコードは、複数回適用可能
exectimeは秒単位でセットされる

{
    "event": true,
    "data": {
        "coupon_info": [{
            "id": "1",
            "coupon": "100",
            "charge_id": "17",
            "data_product_id": "mvno_add_00100",
            "data_charge_name": "追加チャージ100MB",
            "data_price": "200",
            "data_price_full": "216",
            "is_onetime": false
        }, {
            "id": "2",
            "coupon": "500",
            "charge_id": "18",
            "data_product_id": "mvno_add_00500",
            "data_charge_name": "追加チャージ500MB",
            "data_price": "600",
            "data_price_full": "648",
            "is_onetime": false
        }, {
            "id": "3",
            "coupon": "1000",
            "charge_id": "19",
            "data_product_id": "mvno_add_01000",
            "data_charge_name": "追加チャージ1000MB",
            "data_price": "1100",
            "data_price_full": "1188",
            "is_onetime": false
        }, {
            "id": "4",
            "coupon": "1000",
            "charge_id": "77",
            "data_product_id": "mvno_add_onetime_01000",
            "data_charge_name": "追加チャージ1000MB※繰り越しなし",
            "data_price": "480",
            "data_price_full": "518",
            "is_onetime": true
        }]
    },
    "exectime": 0.0074,
    "memory": "1,532,128"
}





Mvno_IijConnect.GetCouponStatus:クーポンステータス問合せ


Resuest:

messageにAPI名”Mvno_IijConnect.GetCouponStatus”を指定する

POST https://www.dmm.com/service/-/json/=/method=AndroidApp HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: www.dmm.com
Connection: Keep-Alive
Accept-Encoding: gzip
 
[POST Params]
authkey %AUTH_KEY%
appid   android_mvno
message Mvno_IijConnect.GetCouponStatus
params  {"access_token":"%ACC_TOKEN%","kpg_code":"kpg%KPG_CODE%"}




Response:

正常終了の場合、data/statusに200がセットされる

{
    "event": true,
    "data": {
        "status": "200",
        "coupon_status": true
    },
    "exectime": 1.329,
    "memory": "1,532,640"
}





Mvno_IijConnect.GetCouponAmount:残高速通信量問合せ


Resuest:

messageにAPI名”Mvno_IijConnect.GetCouponAmount”を指定する

POST https://www.dmm.com/service/-/json/=/method=AndroidApp HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: www.dmm.com
Connection: Keep-Alive
Accept-Encoding: gzip
 
[POST Params]
authkey %AUTH_KEY%
appid   android_mvno
message Mvno_IijConnect.GetCouponAmount
params  {"access_token":"%ACC_TOKEN%","kpg_code":"kpg%KPG_CODE%"}




Response:

bundle_couponに、契約プランがセットされる(3GBプランの場合は3000がセットされる)
coupon_amountに、利用可能な高速通信量がMB単位でセットされる
coupon_amount_detailは、利用期限毎の通信容量が指定される。
毎月付与される通信クーポンは翌月末まで有効なため、2/1に付与されるクーポンの期限は3/31となる。

{
    "event": true,
    "data": {
        "status": "200",
        "bundle_coupon": "3000",
        "coupon_amount": 1930,
        "coupon_amount_detail": [{
            "Expiration": "20160131",
            "Coupon": "0"
        }, {
            "Expiration": "20160229",
            "Coupon": "1930"
        }, {
            "Expiration": "20160331",
            "Coupon": "0"
        }, {
            "Expiration": "20160430",
            "Coupon": "0"
        }]
    },
    "exectime": 0.8132,
    "memory": "1,432,412"
}








その他API/WebサイトURL


請求内容ページ: https://mvno.dmm.com/mypage/-/status/log/?mvnoga=billing

高速データ通信残量通知: https://mvno.dmm.com/mypage/-/notification/?mvnoga=notice

チャージ履歴: https://mvno.dmm.com/mypage/-/charge/log/

よくある質問: http://help.dmm.com/-/list/=/mid=521/

高速通信のON/OFF制御: https://www.dmm.com/service/-/json/=/method=AndroidApp

messageにAPI名"Mvno_IijConnect.ChangeCouponStatus"を指定してコールする
paramsとして渡すJSONをaction:0として渡すと高速通信OFF,action:1だとONになる。



おしらせ: https://www.dmm.com/service/-/json/=/method=AndroidApp

{
    "event": true,
    "data": {
        "version": "5",
        "current_version": "5",
        "message": "アップデート情報があります。\n\n・高速データ通信残量表示をMB(GB)に変更しました。\n\n・追加チャージボタンを誤って押さないようにデフォルト未選択に変更しました。",
        "redirect_url": https://play.google.com/store/apps/details?id=com.dmm.app.mvno"
    },
    "exectime": 0.0023,
    "memory": "884,741"
}



アプリ一覧: http://app-api.dmm.com/applist/v1/apps

{
    "result": "OK",
    "title": "DMM Apps",
    "banner_list": [{
        "banner_id": "44",
        "image_url": "http://pics.dmm.co.jp/appapi/app_images/ba_%KEY%.jpg",
        "url": "https://play.google.com\/store\/apps\/details?id=com.dmm.make.OrderCase"
    }, {
        "banner_id": "24",
        "image_url": http://pics.dmm.co.jp/appapi/app_images/ba_%KEY%.jpg",
        "url": "https:\/\/play.google.com\/store\/apps\/details?id=jp.nikukai\u0026hl=ja"
    }],
 
    "icon_list": [{
        "icon_id": "59",
        "image_url": "http://pics.dmm.co.jp/appapi/app_images/ic_%GUID%.png",
        "name": "DMM.E",
        "description": "チケット申し込みから入場まで...",
        "price": "0",
        "badge": "2",
        "app_info": "com.dmm.app.event"
    }, {
        "icon_id": "57",
        "image_url": "http://pics.dmm.co.jp/appapi/app_images/ic_%GUID%.png",
        "name": "DMM.yell",
        "description": "有名人の投稿写真や動画の閲覧...",
        "price": "0",
        "badge": "0",
        "app_info": "com.dmm.sub.yell"
    }]
}

[C#]LabelなどGUIコントロールの動的生成コードを一瞬で作る方法

VisualStudioでWinFormを使って画面を作る際、通常はボタンやテキストボックスなどのGUIパーツ(コントロール)を画面から設定していきます。
ツールボックスからパーツをドラッグして、プロパティから各種設定を行うのが通常のパターンですね。


時にはこのコントロールを動的に作成したい場合があります。各コントロールは単なるクラスなので、このような場合でも、動的生成したいコントロールのクラスをnewしてプロパティをセットするプログラムを書けば問題なく動的生成が行えます。

ですが、動的にコントロールを作る場合、何のプロパティをセットしたら自分が思ったような設定になるのか、試行錯誤するのが結構大変です。

このような場合は下記の手順で作業すると、簡単かつ速くコントロールを動的生成するコードを書けます。

コントロールを動的生成するメソッドの作り方


まずは、VisualStudioから、今までどおりコントロールを作ります。
今回は、LabelをForm上に置き、背景色やサイズ、ラベルテキストなどを修正しました。



プロパティのペインで見ると、黒字になっている項目が変更している箇所です。


上の画像だとちょっと見づらいですが、下記のプロパティを変えています。

Name
BackColor
BoarderStyle
Location
Size
TabIndex
Text
TextAlign




次に、ソリューションエクスプローラに表示されているファイル一覧からForm1.Designer.csをダブルクリックしてソースを表示させます。


ファイル中にあるInitializeComponent()メソッドの中を見ると、先ほど設定したlabelの設定内容がC#のプログラムとして表示されています。

namespace Project1
{
    partial class Form1
    {
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        private System.ComponentModel.IContainer components = null;
        ...
 
        #region Windows フォーム デザイナーで生成されたコード
        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.label1 = new System.Windows.Forms.Label();
 
            // 
            // label1
            // 
            this.label1.BackColor = System.Drawing.Color.MistyRose;
            this.label1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.label1.Location = new System.Drawing.Point(12, 9);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(40, 40);
            this.label1.TabIndex = 3;
            this.label1.Text = "65535";
            this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
 
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(256, 256);
            this.Controls.Add(this.label1);
            ...
        }
        #endregion
 
        private System.Windows.Forms.Label label1;
    }
}



今回はnameをlabel1として作ったので、label1に関する定義だけを抽出してメソッドにします。
今回の例では分かりやすいよう、以下のように名前をlabel1からmyLabelに変えてみました。

private System.Windows.Forms.Label myLabel;
 
private void createCustomLabel()
{
    this.myLabel = new System.Windows.Forms.Label();
 
    this.myLabel.BackColor = System.Drawing.Color.MistyRose;
    this.myLabel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
    this.myLabel.Location = new System.Drawing.Point(12, 9);
    this.myLabel.Name = "myLabel";
    this.myLabel.Size = new System.Drawing.Size(40, 40);
    this.myLabel.TabIndex = 3;
    this.myLabel.Text = "65535";
    this.myLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
 
    this.Controls.Add(this.myLabel);
}



メソッド化が終わったら、元のlabel1はFormから削除してしまいます。


次に、このメソッドをコールします。
たとえば、ボタンがクリックされたらラベルを生成したいのであれば、以下のような感じになります。

private void button1_Click(object sender, EventArgs e)
{
    createCustomLabel();
}



プログラムを実行し、ボタンをクリックすると、想定どおりボタンクリックをトリガにラベルが動的生成されます。


もう少し補足


コントロールにイベントハンドラを指定したい場合も、同じようにイベントハンドラを設定してからForm1.Designer.csをコピペすればOKです。
以下のような感じで、イベントハンドラの追加処理が記載されているはずです。

private void InitializeComponent()
{
    ...
    this.label1.Click += new System.EventHandler(this.label1_Click);
}




実行してみてもうまく表示されない場合は、他のパーツの裏に隠れているだけの可能性があります。下記のメソッドを書くと問題の切り分けになるかもしれません。

myLabel.BringToFront();




コントロールを動的生成させる場所を変更したい場合は、Controls.Addの処理を書き換えます。
Formの直下に生成するのではなく、Formに貼り付けたPanel内に生成したい場合は、以下のような感じにします。

this.Controls.Add(this.myLabel);
↓
panel1.Controls.Add(this.myLabel);




複数のラベルを動的に作りたい場合は、変数を配列やListにすればOKです。

private System.Windows.Forms.Label myLabel;private List<System.Windows.Forms.Label> newLabel = new ...;



C#で2048ゲームのクローンを作る[その4]

今日は、前回の予定通り盤面の管理を別クラスに分けてみました。
リファクタリングを行っただけなので、前回のプログラムとできることは全く同じです。


まずは、画面を管理しているForm1.csファイルです。
盤面管理をBoardManagerに逃がしたので、非常にシンプルになりました。

using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
 
namespace Mock2048 {
    public partial class Form1 : Form {
        BoardManager boardMgr;
 
        public Form1() {
            InitializeComponent();
 
            // ボードマネージャの生成
            boardMgr = new BoardManager();
        }
 
        //*********************************************************************
        /// <summary> 画面表示時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_Load( object sender, EventArgs e ) {
 
            // Formコントロール自身がキー入力を取得可能とする
            this.KeyPreview = true;
 
            //盤面の初期化
            boardMgr.initBoard();
 
            // 盤面を表示する
            boardMgr.displayBoard(txtLog); 
        }
 
 
        //*********************************************************************
        /// <summary> キー入力時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_KeyDown( object sender, KeyEventArgs e ) {
            DIR direction;
 
            // ゲームオーバーのときは何もしない
            if (boardMgr.IsGameOver) {
                return;
            }
 
            // スライド方向を判定する
            switch (e.KeyCode) {
                case Keys.Up:    direction = DIR.UP;    break;
                case Keys.Right: direction = DIR.RIGHT; break;
                case Keys.Down:  direction = DIR.DOWN;  break;
                case Keys.Left:  direction = DIR.LEFT;  break;
                default:
                    // 矢印キー以外は無視
                    return; 
            }
 
            // 指定された方向に移動させる
            bool isMove = boardMgr.slideCell( direction );
            if ( !isMove ) {
                // スライドさせてみたが1セルも動かなかった -> 何もしなかったとみなす
                return;
            }
 
            // 盤面を表示する
            boardMgr.displayBoard(txtLog);
        }
    }
}





次にBoardManager.csです。
こちらは、メソッドを移した上でpublic/privateのスコープを見直しています。

using System;
using System.Collections.Generic;
using System.Diagnostics;
 
public enum DIR {
    UP,
    RIGHT,
    DOWN,
    LEFT,
};
 
public class BoardManager {
    const int BOARD_SIZE = 4;
 
    // ゲームの状態(ゲームオーバーしたか否か)
    private bool isGameOver = false;
    public  bool IsGameOver {
        get{ return this.isGameOver; }
    }
 
    // 乱数
    System.Random rand = new Random();
 
    // 盤面の情報
    private int[,] board = new int[BOARD_SIZE,BOARD_SIZE];      
 
    //*********************************************************************
    /// <summary> 盤面の初期化を行う
    /// </summary>
    //*********************************************************************
    public void initBoard() {
        isGameOver = false;
 
        // ランダムに2マス埋める
        for (int loop = 0; loop < 2; loop++) {
            _addCell();
        }
    }
 
    //*********************************************************************
    /// <summary> 盤面を画面に表示させる
    /// </summary>
    //*********************************************************************
    public void displayBoard( System.Windows.Forms.TextBox displayArea ) {
 
        // ゲームオーバーのときは描画しない
        if (isGameOver) {
            displayArea.Text = "GAME OVER";
            return;
        }
 
        string boardData = "";
 
        // すべての行のデータを出すまで繰り返し
        for (int y = 0; y < 4; y++) {
            // 1行分のデータを出力
            for (int x = 0; x < 4; x++) {
                boardData += "[" + String.Format("{0, 2}", board[x,y]) + "] ";
            }
            boardData += Environment.NewLine;
            boardData += Environment.NewLine;
        }
 
        displayArea.Text = boardData;
 
        log( "** Boardを描画します**" );
        log( boardData );
 
        displayArea.Select(0,0);
        displayArea.ReadOnly = true;
    }
 
    //*********************************************************************
    /// <summary> 指定された方向に移動させる
    /// </summary>
    /// <param name="keyCode"></param>
    //*********************************************************************
    public bool slideCell( DIR dir ) {
        bool isMove = false;
        bool retVal;
 
        // 入力されたキーを判定する
        switch ( dir ) {
            case DIR.UP:
                // 2~4列目に対し上方向のスライドを試みる
                for (int y = 1; y < BOARD_SIZE; y++) {
                    for (int x = 0; x < BOARD_SIZE; x++) {  
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.DOWN:
                // 1~3列目に対し下方向のスライドを試みる
                for (int y = BOARD_SIZE-2; y >= 0; y--) {
                    for (int x = 0; x < BOARD_SIZE; x++) {  
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.LEFT:
                // 2~4行目に対し左方向のスライドを試みる
                for (int y = 0; y < BOARD_SIZE; y++) {
                    for (int x = 1; x < 4; x++) {
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.RIGHT:
                // 1~3行目に対し右方向のスライドを試みる
                for (int y = 0; y < BOARD_SIZE; y++) {
                    for (int x = BOARD_SIZE-2; x >= 0; x--) {
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            default:
                //txtLog.Text = "";
                break;                        
        }
        log( "セルをスライドさせました 方向=" + dir.ToString() + " 移動あり?=" + isMove );
 
 
        // セルを追加する
        bool isSuccess = _addCell();
        if (!isSuccess) {
            // セルが追加できない(=空セルがない)時は、ゲームオーバーとみなす
            isGameOver = true;
        }
 
        // 追加した結果、まだスライド可能かチェック
        if (!_canSlide()) {
            // どの方向にもスライドできない場合は、詰みなのでゲームオーバー
            isGameOver = true;
        }
 
        return isMove;
    }
 
 
    //*********************************************************************
    /// <summary> 空いているところに、1つセルを追加する
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private bool _addCell() {
 
        // あいているセルを1つ取得する
        Tuple<int,int> freePos = _getEmptyCell_AtRandom();
        if (freePos == null) {
            return false; // 空セル無し
        }
 
        // 取得した空セルにセットする
        int val = rand.Next(1,3) * 2;
        board[freePos.Item1,freePos.Item2] = val;
 
        log( "  -> セットする値=" + val );
 
        return true;
    }
 
 
    //*********************************************************************
    /// <summary> 空セルをランダムで1つ取得する
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private Tuple<int,int> _getEmptyCell_AtRandom() {
        List<Tuple<int,int>> emptyCellList = new List<Tuple<int,int>>();
 
        // 空セルを全部Listに集める
        for (int y = 0; y < BOARD_SIZE; y++) {
            for (int x = 0; x < BOARD_SIZE; x++) {
                if ( board[x,y] == 0 ) {
                    emptyCellList.Add( new Tuple<int,int>(x,y) );
                }
            }
        }           
        if (emptyCellList.Count <= 0) {
            // 空セルが1つも無い! ->nullを返して終了。
            return null; 
        }
 
        // 空セルの中から1つをランダムで抽選する
        int offset = rand.Next(0, emptyCellList.Count-1);
        Tuple<int,int> emptyCell = emptyCellList[offset];
 
        log( "空セルを取得しました [" + emptyCell.Item1 + ", " + emptyCell.Item2 + "] 残りの空数=" + (emptyCellList.Count-1) );
        return emptyCell;
    }
 
 
    //*********************************************************************
    /// <summary> セルのスライドが可能がチェックする
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private bool _canSlide() {
        // 上下に連続して同じ数字があるかチェック
        for (int x = 0; x < BOARD_SIZE; x++) {
            for (int y = 0; y < BOARD_SIZE; y++) {
                if (board[x, y] == 0) {
                    // 空セルがある -> スライド可能
                    return true;
                }
 
                if (x != BOARD_SIZE-1 && board[x, y] == board[x+1, y]) {
                    // 右に同じ数字が続く場所がある -> スライド可能
                    return true;
                }
                if (y != BOARD_SIZE-1 && board[x, y] == board[x, y+1]) {
                    // 下に同じ数字が続く場所がある -> スライド可能
                    return true;
                }
 
            }
        }
 
        // ここまできた -> どの方向にもスライドできない
        return false;
    }
 
 
    //*********************************************************************
    /// <summary> 指定された方向にセルをスライドさせる
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="dir"></param>
    //*********************************************************************
    private bool _moveCell( int x, int y, DIR dir ) {
        int endX;
        int endY;
        bool isDuplicate;
 
        // 移動先のセル位置を求める
        _getDestPos( x, y, dir, out endX, out endY, out isDuplicate );
 
        // セルを移動
        int targetVal = board[x,y];
        board[x,y] = 0;
        board[endX,endY] = targetVal * (isDuplicate ? 2 : 1 );
 
        // セルが動いたか否かを返す
        bool isMove;
        if (x == endX && y == endY) {
            isMove = false;
        } else {
            isMove = true;
        }
 
        return isMove;
    }
 
 
    //*********************************************************************
    /// <summary> 指定された方向にセルをスライドさせた時の移動先を求める
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="dir"></param>
    /// <param name="endX"></param>
    /// <param name="endY"></param>
    /// <param name="isDuplicate"></param>
    //*********************************************************************
    private void _getDestPos( int x, int y, DIR dir, out int endX, out int endY, out bool isDuplicate ) {
 
        // initialize
        endX = x;
        endY = y;
        isDuplicate = false;
 
        // 指定されたセルの値を取得
        int targetCellVal = board[x,y];
        if (targetCellVal == 0) {
            return;
        }
 
        // 移動方向を元に探索方向のオフセットを決める
        int dx = 1;
        int dy = 1;
        switch ( dir ) {
            case DIR.UP:    dx =  0; dy = -1; break;
            case DIR.RIGHT: dx =  1; dy =  0; break;
            case DIR.DOWN:  dx =  0; dy =  1; break;
            case DIR.LEFT:  dx = -1; dy =  0; break;
        }
 
 
        // 何かにぶつかるまで探索する
        while (true) {
            if (endX + dx < 0 || endY + dy < 0 || endX + dx >= BOARD_SIZE || endY + dy >= BOARD_SIZE) {
                // 壁にぶつかった -> 1つ手前の場所でストップ
                isDuplicate = false;
                break;
            } else if (board[endX + dx, endY + dy] != 0) {
                // 他のセルにぶつかった
                if (targetCellVal == board[endX + dx, endY + dy]) {
                    // 衝突先が同じ値だった -> 重ねる
                    endX += dx;
                    endY += dy;
                    isDuplicate = true;
                    break;
                } else {
                    // 衝突先が異なる値だった -> 1つ手前の場所でストップ
                    isDuplicate = false;
                    break;
                }
            } else {
                // 何にもぶつからなかった -> さらに次の位置をチェック
                endX += dx;
                endY += dy;
                continue;
            }               
        }
        //log( "セルの移動を行います [" + x + ", " + y + "] -> [" + endX + ", " + endY + "] dup=" + isDuplicate );
    }
 
    //*********************************************************************
    /// <summary> デバッグログを出力する
    /// </summary>
    /// <param name="message"></param>
    //*********************************************************************
    private void log(string message) {
        Debug.WriteLine( message );
    }
}




次回は、ボードに表示されるセル(数字が表示されている駒)のクラス管理と、描画処理を見直す予定です。

C#で2048ゲームのクローンを作る[その3]

今回は、ゲームサイクルの作成を行いました。

以下の細々とした処理を追加し、1回分のゲーム開始->プレイ->ゲームオーバーまで、1周分のサイクルが作れました。

ゲーム状態の管理を追加(isGameOver変数)
KeyDown()に、ゲームオーバー判定を追加
KeyDown()に、1手動かすたびに新セルの生成処理を追加
initBoard()に、盤面の初期化処理(ランダムで2セル生成)を追加
addCell()として、新セルの生成のメソッドを追加
slideCell()に、スライドした結果セルの移動があったか否かを取得できるよう修正





一通り遊べるのですが、スライドさせたときのアニメーションが無いため非常に遊びづらいです。
よく見ながら操作しないと、特に、どのセルがどこに移動したのか非常に分かり辛いです。

ですので、次は表示の改善を行いたいところですが…
一方で、現状の1ファイルでの管理はそろそろ限界に近づいてきてます。
なので、次回は盤面を管理するBoardクラスの切り出しを行おうと考えています。


修正後のコードは以下のとおりです。
ちょっと長くなってきました…

using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
using System.Diagnostics;
 
namespace Mock2048 {
    enum DIR {
        UP,
        RIGHT,
        DOWN,
        LEFT,
    };
 
    public partial class Form1 : Form {
        const int BOARD_SIZE = 4;
 
        // ゲームの状態管理(ゲームオーバーしたか否か)
        bool isGameOver = false;
 
        // 乱数
        System.Random rand = new Random();
 
        // 盤面の情報
        private int[,] board = new int[BOARD_SIZE,BOARD_SIZE];      
 
        public Form1() {
            InitializeComponent();
        }
 
        //*********************************************************************
        /// <summary> 画面表示時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_Load( object sender, EventArgs e ) {
            // Formコントロール自身がキー入力を取得可能とする
            this.KeyPreview = true;
 
            //盤面の初期化
            initBoard();
 
            // 盤面を表示する
            displayBoard(); 
        }
 
 
        //*********************************************************************
        /// <summary> キー入力時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_KeyDown( object sender, KeyEventArgs e ) {
            DIR direction;
 
            // ゲームオーバーのときは何もしない
            if (isGameOver) {
                return;
            }
 
 
            // 指定された方向に移動させる
            switch (e.KeyCode) {
                case Keys.Up:    direction = DIR.UP;    break;
                case Keys.Right: direction = DIR.RIGHT; break;
                case Keys.Down:  direction = DIR.DOWN;  break;
                case Keys.Left:  direction = DIR.LEFT;  break;
                default:
                    // 矢印キー以外は無視
                    return; 
            }
            bool isMove = slideCell( direction );
            if ( !isMove ) {
                // スライドさせてみたが1セルも動かなかった -> 何もしなかったとみなす
                return;
            }
 
 
            // セルを追加する
            bool isSuccess = addCell();
            if (!isSuccess) {
                // セルが追加できない(=空セルがない)時は、ゲームオーバーとみなす
                isGameOver = true;
            }
 
            // 追加した結果、まだスライド可能かチェック
            if (!canSlide()) {
                // どの方向にもスライドできない場合は、詰みなのでゲームオーバー
                isGameOver = true;
            }
 
            // 盤面を表示する
            displayBoard();
        }
 
        //*********************************************************************
        /// <summary> 盤面の初期化を行う
        /// </summary>
        //*********************************************************************
        private void initBoard() {
            isGameOver = false;
 
            // ランダムに2マス埋める
            for (int loop = 0; loop < 2; loop++) {
                addCell();
            }
        }
 
        //*********************************************************************
        /// <summary> 空いているところに、1つセルを追加する
        /// </summary>
        /// <returns></returns>
        //*********************************************************************
        private bool addCell() {
 
            // あいているセルを1つ取得する
            Tuple<int,int> freePos = getEmptyCell_AtRandom();
            if (freePos == null) {
                return false; // 空セル無し
            }
 
            // 取得した空セルにセットする
            int val = rand.Next(1,3) * 2;
            board[freePos.Item1,freePos.Item2] = val;
 
            log( "  -> セットする値=" + val );
 
            return true;
        }
 
        //*********************************************************************
        /// <summary> 空セルをランダムで1つ取得する
        /// </summary>
        /// <returns></returns>
        //*********************************************************************
        private Tuple<int,int> getEmptyCell_AtRandom() {
            List<Tuple<int,int>> emptyCellList = new List<Tuple<int,int>>();
 
            // 空セルを全部Listに集める
            for (int y = 0; y < BOARD_SIZE; y++) {
                for (int x = 0; x < BOARD_SIZE; x++) {
                    if ( board[x,y] == 0 ) {
                        emptyCellList.Add( new Tuple<int,int>(x,y) );
                    }
                }
            }           
            if (emptyCellList.Count <= 0) {
                // 空セルが1つも無い! ->nullを返して終了。
                return null; 
            }
 
            // 空セルの中から1つをランダムで抽選する
            int offset = rand.Next(0, emptyCellList.Count-1);
            Tuple<int,int> emptyCell = emptyCellList[offset];
 
            log( "空セルを取得しました [" + emptyCell.Item1 + ", " + emptyCell.Item2 + "] 残りの空数=" + (emptyCellList.Count-1) );
            return emptyCell;
        }
 
        //*********************************************************************
        /// <summary> 盤面を画面に表示させる
        /// </summary>
        //*********************************************************************
        private void displayBoard() {
 
            // ゲームオーバーのときは描画しない
            if (isGameOver) {
                txtLog.Text = "GAME OVER";
                return;
            }
 
            string boardData = "";
 
            // すべての行のデータを出すまで繰り返し
            for (int y = 0; y < 4; y++) {
                // 1行分のデータを出力
                for (int x = 0; x < 4; x++) {
                    boardData += "[" + String.Format("{0, 2}", board[x,y]) + "] ";
                }
                boardData += Environment.NewLine;
                boardData += Environment.NewLine;
            }
 
            txtLog.Text = boardData;
 
            log( "** Boardを描画します**" );
            log( boardData );
 
            txtLog.Select(0,0);
            txtLog.ReadOnly = true;
        }
 
        //*********************************************************************
        /// <summary> セルのスライドが可能がチェックする
        /// </summary>
        /// <returns></returns>
        //*********************************************************************
        private bool canSlide() {
            // 上下に連続して同じ数字があるかチェック
            for (int x = 0; x < BOARD_SIZE; x++) {
                for (int y = 0; y < BOARD_SIZE; y++) {
                    if (board[x, y] == 0) {
                        // 空セルがある -> スライド可能
                        return true;
                    }
 
                    if (x != BOARD_SIZE-1 && board[x, y] == board[x+1, y]) {
                        // 右に同じ数字が続く場所がある -> スライド可能
                        return true;
                    }
                    if (y != BOARD_SIZE-1 && board[x, y] == board[x, y+1]) {
                        // 下に同じ数字が続く場所がある -> スライド可能
                        return true;
                    }
 
                }
            }
 
            // ここまできた -> どの方向にもスライドできない
            return false;
        }
 
        //*********************************************************************
        /// <summary> 指定された方向に移動させる
        /// </summary>
        /// <param name="keyCode"></param>
        //*********************************************************************
        private bool slideCell( DIR dir ) {
            bool isMove = false;
            bool retVal;
 
            // 入力されたキーを判定する
            switch ( dir ) {
                case DIR.UP:
                    // 2~4列目に対し上方向のスライドを試みる
                    for (int y = 1; y < BOARD_SIZE; y++) {
                        for (int x = 0; x < BOARD_SIZE; x++) {  
                            retVal = moveCell( x, y, DIR.UP );
                            isMove = (retVal==true) ? true : isMove;
                        }
                    }
                    break;
                case DIR.DOWN:
                    // 1~3列目に対し下方向のスライドを試みる
                    for (int y = BOARD_SIZE-2; y >= 0; y--) {
                        for (int x = 0; x < BOARD_SIZE; x++) {  
                            retVal = moveCell( x, y, DIR.DOWN );
                            isMove = (retVal==true) ? true : isMove;
                        }
                    }
                    break;
                case DIR.LEFT:
                    // 2~4行目に対し左方向のスライドを試みる
                    for (int y = 0; y < BOARD_SIZE; y++) {
                        for (int x = 1; x < 4; x++) {
                            retVal = moveCell( x, y, DIR.LEFT );
                            isMove = (retVal==true) ? true : isMove;
                        }
                    }
                    break;
                case DIR.RIGHT:
                    // 1~3行目に対し右方向のスライドを試みる
                    for (int y = 0; y < BOARD_SIZE; y++) {
                        for (int x = BOARD_SIZE-2; x >= 0; x--) {
                            retVal = moveCell( x, y, DIR.RIGHT );
                            isMove = (retVal==true) ? true : isMove;
                        }
                    }
                    break;
                default:
                    //txtLog.Text = "";
                    break;                        
            }
 
            log( "セルをスライドさせました 方向=" + dir.ToString() + " 移動あり?=" + isMove );
 
            return isMove;
        }
 
        //*********************************************************************
        /// <summary> 指定された方向にセルをスライドさせる
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="dir"></param>
        //*********************************************************************
        private bool moveCell( int x, int y, DIR dir ) {
            int endX;
            int endY;
            bool isDuplicate;
 
            // 移動先のセル位置を求める
            getDestPos( x, y, dir, out endX, out endY, out isDuplicate );
 
            // セルを移動
            int targetVal = board[x,y];
            board[x,y] = 0;
            board[endX,endY] = targetVal * (isDuplicate ? 2 : 1 );
 
            // セルが動いたか否かを返す
            bool isMove;
            if (x == endX && y == endY) {
                isMove = false;
            } else {
                isMove = true;
            }
 
            return isMove;
        }
 
 
        //*********************************************************************
        /// <summary> 指定された方向にセルをスライドさせた時の移動先を求める
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="dir"></param>
        /// <param name="endX"></param>
        /// <param name="endY"></param>
        /// <param name="isDuplicate"></param>
        //*********************************************************************
        private void getDestPos( int x, int y, DIR dir, out int endX, out int endY, out bool isDuplicate ) {
 
            // initialize
            endX = x;
            endY = y;
            isDuplicate = false;
 
            // 指定されたセルの値を取得
            int targetCellVal = board[x,y];
            if (targetCellVal == 0) {
                return;
            }
 
            // 移動方向を元に探索方向のオフセットを決める
            int dx = 1;
            int dy = 1;
            switch ( dir ) {
                case DIR.UP:    dx =  0; dy = -1; break;
                case DIR.RIGHT: dx =  1; dy =  0; break;
                case DIR.DOWN:  dx =  0; dy =  1; break;
                case DIR.LEFT:  dx = -1; dy =  0; break;
            }
 
 
            // 何かにぶつかるまで探索する
            while (true) {
                if (endX + dx < 0 || endY + dy < 0 || endX + dx >= BOARD_SIZE || endY + dy >= BOARD_SIZE) {
                    // 壁にぶつかった -> 1つ手前の場所でストップ
                    isDuplicate = false;
                    break;
                } else if (board[endX + dx, endY + dy] != 0) {
                    // 他のセルにぶつかった
                    if (targetCellVal == board[endX + dx, endY + dy]) {
                        // 衝突先が同じ値だった -> 重ねる
                        endX += dx;
                        endY += dy;
                        isDuplicate = true;
                        break;
                    } else {
                        // 衝突先が異なる値だった -> 1つ手前の場所でストップ
                        isDuplicate = false;
                        break;
                    }
                } else {
                    // 何にもぶつからなかった -> さらに次の位置をチェック
                    endX += dx;
                    endY += dy;
                    continue;
                }               
            }
 
            //log( "セルの移動を行います [" + x + ", " + y + "] -> [" + endX + ", " + endY + "] dup=" + isDuplicate );
        }
 
        //*********************************************************************
        /// <summary> デバッグログを出力する
        /// </summary>
        /// <param name="message"></param>
        //*********************************************************************
        private void log(string message) {
            Debug.WriteLine( message );
        }
    }
}




また、コンパイルしたexeも置いときます。
Mock2048_20160128.zip