Home

技術日記Laravel4PHP
Laravel 4.0.10からLaravel 4.1.5にアップグレードEdit

PHPフレームワークLaravelの4.1がリリースされた。手元に作りかけのLaravel 4アプリケーションがあったんで、今後のことを考えてとっととLaravel 4.1にアップグレードすることにした。手元のLaravel 4フレームワークのバージョンは4.0.10。

アプリケーションのスペックとしては、

  • 使用DBはsqliteのみ。ちょっとしたデートストア程度にしか使っていない。
  • JeffreyWay / Laravel-4-Generatorsで生成したマスタメンテ用Scaffoldページがいくつか。
  • JavaScriptアプリケーションのコンテナWebページが5ページほど。サーバーロジックはほとんどないのでroutes.phpでView::make()しているだけ。
  • メインのJavaScriptアプリケーションから呼ばれるJSON APIが10個ほど。DBとのやりとりがほとんどなので、routes.php内で直接Eloquent ORMを使って処理をして、JSONで結果を返すだけ。
といった感じ。小規模だしLaravel 4の機能は最低限しか使っていないから、移行のハードルは低い。

まずは、4.0から4.1へのアップグレードガイド日本語訳にざっと目を通す。私はLeanpubで日本語ドキュメントを買ってるんで、PDFで読んでるけど。

変更点としては、いくつかのファイルの差し替え、設定ファイル関連の変更がいくつか、コントローラの変更がちょっと。パスワードリマインダーも変更されたらしいけど、使ってないから内容は未チェック。作業ステップは一桁程度か。

まず、composer.jsonのrequireセクションにあるlaravel/frameworkのバージョンを4.0.から4.1.に変更。アップグレードガイドの手順とは違うけど、まずはそのままcomposer updateしてみる。

大量のremove&installの後で、Fatal Errorで終了。エラーは、

Error Output: PHP Fatal error: Call to undefined method Illuminate\Foundat
 ion\Application::redirectIfTrailingSlash() in /Users/ishinao/NetBeansProjec
 ts/webgldance/laravel/bootstrap/start.php on line 16

なんで、アップグレードガイドにある「Trailing Slashリダイレクトの削除」に引っかかったらしい。bootstrap/start.phpから,

$app->redirectIfTrailingSlash();

を削除。もう一度composer updateしたら、今度はエラーなしで正常終了した。

この段階で、試しにWebアプリケーションルートをブラウザで開いてみたところ、200 OKは返るけどコンテンツは何も返らない状態だった。

ということで、アップグレードガイドに書いてある作業を一つずつつぶしていくことにする。

まずはWebアプリケーション起動ファイルを新バージョンの内容に置き換え。public/index.phpを https://raw.github.com/laravel/laravel/master/public/index.phpからダウンロードして差し替え。

同様に、artisanコマンドの内容もhttps://raw.github.com/laravel/laravel/master/artisanからダウンロードした新バージョンの内容に置き換え。

続いて、設定ファイルを新バージョンの内容に追随させていく。

app/config/app.phpの内容をhttps://raw.github.com/laravel/laravel/master/app/config/app.phpの内容を元に書き換える。変わっているのはprovidersとaliasesのところなので、自分で追加/編集した内容がなければ、丸ごと新しい方で置き換える。自分で追加した内容があればそれを残しつつ。

新しく追加されたhttps://raw.github.com/laravel/laravel/master/app/config/remote.phpをapp/config/remote.phpとして保存。

セッション設定ファイル app/config/session.phpに

'expire_on_close' => false,

を追加。あとで、app/storage/logsを見てみたら、

'Undefined index: expire_on_close' in vendor/laravel/framework/src/Illuminate/Session/Middleware.php:171

が残っていたんで、これが起動時に画面が表示されない原因だったっぽい。

新機能の失敗したジョブキューからの復帰用設定を追加。app/config/queue.phpに

  'failed' => array(
    'database' => 'mysql', 'table' => 'failed_jobs',
  ),

を追加。

これでWebアプリケーションルートにブラウザからアクセスしてみたところ、無事画面が表示された。

他にもコントローラ関連の修正項目が書かれていたけれども、Generatorsで自動生成したコントローラクラスは、特に修正する必要がなかった。

アップグレードしたバージョンは4.1.0かと思っていたら、php artisanしてみたら、

Laravel Framework version 4.1.5

になっていた。

追記@2013/12/14

アップグレードガイドがアップデートされ、アップグレード手順で一つ重要な内容が欠けていたので、その部分を追記。

$app->redirectIfTrailingSlash() 相当の処理を.htaccessで行うようになったため、bootstrap/start.phpからそのコードを削除したにもかかわらず、上記手順では.htaccessをLaravel 4のままで使っている。public/.htaccessをhttps://raw.github.com/laravel/laravel/master/public/.htaccessに差し替える必要がある。

ちなみに修正内容として、以下の内容が追加されている。

# Redirect Trailing Slashes...
RewriteRule ^(.*)/$ /$1 [L,R=301]

また、Laravel Framework本体もアップデートされており、composer updateすると、

Laravel Framework version 4.1.8

になっていた。アップデートペース、はやっ。

Published At2013-12-13 15:19Updated At2020-01-01 15:39

技術日記Laravel4PHP
Laravel 4のエラーメッセージとフォームエラーEdit

Laravel 4では、

Redirect::to()->withErrors()

をすると、自動的にリダイレクト後のビュー内で$errorsに展開されるなど、エラーメッセージは特別な扱いになっている。このエラーメッセージは共通レイアウトなんかで、

@if ($errors->count() > 0)
<ul class="error">
@foreach ($errors as $error)
<li>{{{ $error }}}</li>
@endforeach
@endif

なんて感じに表示するのが普通だろうけど、フォームエラーなんかで、入力欄のそばにエラーメッセージを表示したい場合は、どういうふうに使い分けるといいんだろう。

(エラー表示コードを含む)共通レイアウトは使わず、フォーム表示ビューの方で、

{{ Form::open() }}
{{ Form::label('name') }}
{{ Form::text('name') }}
{{ $errors->first('name', '<p class="error">:message</p>') }}
{{ Form::close() }}

みたいな感じに書くのがいいのか。でもそうなると、(エラーメッセージ表示をしたい)フォームを含むかどうかで、レイアウトファイルを(エラー表示コードを含む物と含まない物で)切り替えることになるのが、あんまり気持ちよくない。

あるいは

[source language="php"] Redirect::to()->with('formErrors', $validator->messages) [/source]

なんて感じで、標準エラーメッセージを使わずに、独自のフラッシュセッションに保存して、

[source language="php"] if (Session::has('formErrors')) { View::share('formErrors', Session::get('formErrors'); } [/source]

とかやる方がいいんだろうか。これはこれでありだろうけど、せっかく標準エラーメッセージ機能があるのに、別でハンドリングするのも今ひとつのような気もする。

標準エラーメッセージを使いつつ、フォームビューの方でエラーメッセージを表示した場合は、何らかの方法でフラグをセットして、レイアウトの方のエラーメッセージを抑止するという方法も考えたんだけど、(コンテンツ)ビューから(レイアウト)ビューにデータを渡す、ほどよい方法が見当たらないんで、ペンディング中。

$errorsの実装クラスであるMessageBagにMessageBag::clear()とかが実装されていれば話が早かったんだけど、MessageBagは値を追加したりマージしたりはできても、削除とかリセットとかはできない模様。

今のところ、独自フラッシュセッションを使う方法が一番ましかなーとは思っているんだけど、もっときれいな方法があるという方、情報をお待ちしております。

追記@2013/07/18)

って話は、Laravel 4のwithErrors()は、レイアウトファイルなどを使って、サイト全体で共通表示するフラッシュメッセージ的なものと捉えた上で書いていたんだけど、なんか違うような気がしてきた。

withErrors()で渡すエラーは、共通フラッシュメッセージ的なものではなく、あくまでも特定機能(ページ)でのエラーを手軽にハンドリングするための入れ物なのか? 共通フラッシュメッセージは、with()とかを使った独自処理として書くべき?

たとえば、Laravel-4-Bootstrap-Starter-Site/app/views/notifications.blade.php at master · andrew13/Laravel-4-Bootstrap-Starter-Siteなんかを見ると、共通フラッシュメッセージ的な物としては、success、warning、error、infoを直接Session::flash()(=with())経由でやりとりして表示し、withErrors()(=$errors)はフォームエラー表示専用として扱っているようだ。

withErrors()はあくまでフォームエラー専用の入れ物であって、フラッシュメッセージとは別物、というのが妥当なアプローチなのかな。なんかフォームエラー専用の代物を、フレームワーク標準で自動的にビュー変数にまで代入しちゃうのは、やり過ぎ感が否めないんだけどなー。

Published At2013-07-17 19:07Updated At2020-01-01 15:40

技術日記Laravel4PHP
Laravel 4でfacebookログイン機能も作ってみるEdit

Twitterログイン機能を作ってみたついでに、facebookログイン機能も作ってみる。ただfacebookはあんまり好きじゃないんで、APIのドキュメントを眺めてみたことがある程度しか知識がないから、Laravel 4以前に手探りで実装することになるんだけど。

まず、facebookのPHP SDKをインストールする。composer.jsonのrequireに

    "facebook/php-sdk": "dev-master"

を追加して、composer updateするとfacebook PHP SDKがインストールされる。

続いて、facebookアプリケーションの登録。facebook developersで新しいアプリを作成し、アプリのIDとシークレットキーを取得しておく。その設定は、app/config/facebook.phpとして、

<?php

return array(
    'appId' => '[アプリID]',
    'secret' => '[シークレットキー]',
);

って感じで保存しておくと、Laravel 4アプリ内ではConfig::get('facebook')で取得できる。このあたりの設定ファイルの扱いもLaravel 4はシンプルでいいね。環境(本番/開発など)ごとにオーバーライドさせたければ、環境名のディレクトリを掘ってその中に入れればいいだけだし。

続いてDBの準備。Twitterログインと同じように、usersテーブルに直接facebookのユーザー情報を保持する設計にする。

        Schema::create('users', function($table) {
            $table->biginteger('id')->unsigned();
            $table->primary('id');
            $table->string('name');
            $table->string('access_token');
            $table->timestamps();
        });

facebookのユーザーIDは桁数が多いっぽいんでbigintegerにしておいた。あとは名前とアクセストークンを保存するようにしておく。

ログインルーティングは以下のような感じ。

Route::get('login', function() {
    $facebook = new Facebook(Config::get('facebook'));
    $config = array(
        'redirect_uri' => url('/login/callback'),
    );
    return Redirect::to($facebook->getLoginUrl($config));
});

facebook側の認証ページにリダイレクトさせ、その結果をコールバックURLに返してもらう。続いてコールバックのルーティング。

Route::get('login/callback', function() {
    $code = Input::get('code');
    if (strlen($code) == 0) {
        return Redirect::to('/')->with('message', 'ログインできませんでした。');
    }

    $facebook = new Facebook(Config::get('facebook'));
    $user_id = $facebook->getUser();

    if ($user_id == 0) {
        return Redirect::to('/')->with('message', 'ログインできませんでした。');
    }

    $user = User::find($user_id);
    if (empty($user)) {
        $user = new User;
        $user->id = $user_id;
    }

    $me = $facebook->api('/me');
    $user->name = $me['name'];
    $user->access_token = $facebook->getAccessToken();
    $user->save();

    Auth::login($user);

    return Redirect::to('/')->with('message', 'ログインしました。');
});

facebookログインの仕組みの全貌が把握できていないので、本当にこれでいいのかいまいち確信が持てていないが、少なくとも認証自体はできているはず。

Twitterの場合と同様に、最終的にはAuth::login()でLaravel側での認証情報をセットしている。

ログアウトは、Laravel側のログアウト処理だけ。

Route::get('logout', function() {
    Auth::logout();
    return Redirect::to('/');
});

ホーム画面にfacebookに登録されている名前くらいは表示するようにしておく。

Route::get('/', function()
{
    $data = array();

    if (Auth::check()) {
        $facebook = new Facebook(Config::get('facebook'));
        $me = $facebook->api('/me');
        $data['me'] = $me;
    }

    return View::make('home', $data);
});
// app/views/home.blade.php
@if (!empty($me))
    Hello, {{{ $me['name'] }}}
    <a href="/logout">ログアウト</a>
@else
    <a href="/login">ログイン</a>
@endif

今回はLaravel 4側の問題ではなくfacebook APIの仕組みを調べるのに手間取ってしまった。ちゃんと使うんだったら、もうちょっとfacebook APIのドキュメントを読み込まないとだめだなー。

Published At2013-07-12 15:45Updated At2020-01-01 15:42

技術日記Laravel4PHP
Laravel 4でTwitterログイン機能を作ってみるEdit

Laravel 4で、標準のAuth関連機能とTwitterのOAuth認証パッケージとを連動させて、Twitterでログイン&ユーザー登録機能を作ってみる。

Twitterのユーザー情報をそのまま自前のユーザーテーブルに保存して利用するんで、

php artisan migrate:make create_users_table

をしてmigrationファイルを生成し、以下のようなテーブルスキーマを定義する。

    public function up()
    {
        Schema::create('users', function($table) {
            $table->integer('id')->unsigned();
            $table->primary('id');
            $table->string('screen_name');
            $table->string('oauth_token');
            $table->string('oauth_token_secret');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('users');
    }

プライマリーキーのusers.idに、Twitterのuser_idを直接保存して利用するので、$table->increments('id')にはせず(auto incrementにはせず)、unsigned intなプライマリーキーとして定義する。あと、Twitterの認証情報であるoauth_tokenとoauth_token_secretを保存するカラムも用意しておく。

Userモデルは以下のような感じ。

use Illuminate\Auth\UserInterface;

class User extends Eloquent implements UserInterface {

    public function getAuthIdentifier()
    {
        return $this->getKey();
    }

    public function getAuthPassword()
    {
        return null;
    }

}

Authで認証に利用するユーザークラスにはimplements UserInterfaceが必要なので、getAuthIdentifier()とgetAuthPassword()の二つを追加しておく。Twitter認証ではシステム独自のパスワードは必要ないのでgetAuthPassword()ではNULLを返している。

続いて、Twitter認証パッケージのTwitterOAuth Service Provider for Laravel 4をインストールする。インストール手順はドキュメント通り。composer.jsonに依存設定を追加して、composer updateしたり、config:publishしたり。

あと、dev.twitter.comでアプリケーション登録して認証キーの発行をおこない、app/config/packages/philo/twitter/config.phpにその値を登録しておく。

さてここからが実際のコーディング。Twitterログイン用のルーティング定義は以下のような感じになる。ほぼドキュメントに書かれている通り。

Route::get('login', function() {
    if (Auth::check()) {
        return Redirect::to('/')->with('message', 'ログイン済みです。');
    }
    $tokens = Twitter::oAuthRequestToken();
    Twitter::oAuthAuthorize(array_get($tokens, 'oauth_token'));
    die;
});

これで/loginにアクセスすると、Twitterの認証ページにリダイレクトする。Twitter側で認証されると呼ばれるコールバックURLは、dev.twitter.comで設定できるので、サンプルアプリの「/login/callback」に設定しておく。コールバック時の処理は以下のようになる。

Route::get('login/callback', function() {
    $token = Input::get('oauth_token');
    $verifier = Input::get('oauth_verifier');
    $accessToken = Twitter::oAuthAccessToken($token, $verifier);

    if (isset($accessToken['user_id'])) {
        $user_id = $accessToken['user_id'];
        $user = User::find($user_id);
        if (empty($user)) {
            $user = new User;
            $user->id = $user_id;
        }
        $user->screen_name = $accessToken['screen_name'];
        $user->oauth_token = $accessToken['oauth_token'];
        $user->oauth_token_secret = $accessToken['oauth_token_secret'];
        $user->save();

        Auth::login($user);

        return Redirect::to('/');
    } else {
        return Redirect::to('login')->with('message', 'Twitter認証できませんでした。');
    }
});

コールバックされたパラメータからアクセストークンを取得し、TwitterユーザーID($accessToken['user_id'])が取れていればログイン成功。そのTwitterユーザーIDがusersテーブルに登録されていない場合は、新しいレコードとして登録する。

screen_name、oauth_token、oauth_token_secretに関しては、ログインするたびに新しい情報に更新する。

で、Auth::login($user)がポイントで、これによってそのユーザーでログインしている状態をシステム(セッション)に記憶させる。

今回はusersテーブルに直接Twitterのログイン情報を持たせたんで、Twitter認証情報から直接Userクラスのインスタンスを取得できた。

もしも、Twitter認証情報をusersテーブル以外に持たせたい場合(通常は自前のID、パスワードでログインしつつ、必要ならばTwitterでもログインできるようにする、など)でも、最終的にUserクラス(というかUserIntafaceが実装されたクラス)のインスタンスを取得してしまえば同様にログイン処理が行える。

擬似コードとしては、以下のような感じ。

// Twitter認証テーブルから該当行を取得
$twitterUser = TwitterUser::find($accessToken['user_id']);

// Twitter認証テーブルとリンクしているUserテーブルの該当行を取得
$user = User::find($twitterUser['user_id']); 

Auth::login($user); // 該当ユーザーをログインさせる

一応ログアウト処理のルーティングも書いておく。これはTwitter認証とは関係なく、Laravel 4のログアウト処理のまま。

Route::get('logout', function() {
   Auth::logout();
   return Redirect::to('/')->with('message', 'ログアウトしました。');
});

あと、ホーム画面でログイン状況がわかるように書いておく。

Route::get('/', function()
{
    return View::make('home');
});
// app/views/home.blade.php
@if (Auth::check())
    {{{ Auth::user()->screen_name }}}ログイン中
    <a href="/logout">ログアウト</a>
@else
        未ログイン
    <a href="/login">ログイン</a>
@endif

未ログイン状態の場合は、ログインへのリンクを表示。ログイン中の場合は、ログインユーザーのscreen_nameを表示し、ログアウトリンクを表示している。

ちなみにこのTwitter認証パッケージは、Twitter APIパッケージを利用しているので、このままTwitter APIの機能を利用できる。たとえば、

$user = Auth::user();
Twitter::setOAuthToken($user->oauth_token);
Twitter::setOAuthTokenSecret($user->oauth_token_secret);
$timeline = Twitter::statusesUserTimeline($user->id);

なんて感じで、ユーザーのタイムラインを取得できる。

Laravel 4の認証処理周りはシンプルで、自前でいろいろ処理したい場合でも、なかなか使いやすそうだね。

Published At2013-07-11 17:34Updated At2020-01-01 15:44

技術日記Laravel4
Laravel 4でキューを使ってみるEdit

キューエンジンとして、beanstalkdを使うんで、

yum install beanstalkd
service beanstalkd start

とかやってbeanstalkdをデフォルト設定で動かしておく。

Laravel 4のapp/config/queue.phpで、

'default' => 'beanstalkd',

に変更する。beanstalkdを使うには追加でライブラリをインストールしておく必要があるんで、composer.jsonのrequireに、

"pda/pheanstalk": "2.0.*"

を追加して、composer updateしてインストールしておく。

これでbeanstalkdコネクタを使ってキューを使う準備OK。

キューを使うサンプルとしては、単純なDBカウンタを作ってみる。

事前準備として、app/config/database.phpを書き換えて、mysqlデータベースが動くように適当にしておく。続いて、

php artisan migrate:make create_counter_table

で、カウンター用のテーブルを作る。app/database/migrations/*_create_counter_table.phpに、

  public function up()
  {
    Schema::create('counters', function ($table)
    {
        $table->increments('id');
        $table->integer('value');
        $table->integer('total');
        $table->timestamps();
    });
  }

としておく。要は、

create table counters (
id integer primary key,
value integer,
total integer,
created_at datetime,
updated_at datetime
);

で、valueに加算する整数値、totalにそこまでのvalueの合計が入るイメージ。

これを操作するモデルも作っておく。app/models/Counter.phpとして、

<?php
class Counter extends Eloquent
{
}

を作っておく。

で、まずはcounterにキューを使わずに(同期処理で)カウントするAPIを用意する。app/routes.phpに以下のルートを追加する。

Route::get('count/{value?}', function ($value = null) {
    $value = is_null($value) ? rand(1, 5) : $value;
    $total = Counter::sum('value') + $value;
    sleep(rand(0, 2));
    $counter = Counter::create(array('value' => $value, 'total' => $total));
    $counter->save();
    return $total;
})->where(array('id' => '\d+'));

http://[sample_app_host]/countにアクセスすると、現在DBに登録されているvalueの合計に、1~5までのランダムな整数が加算され、その結果の数値が表示される。

ちなみに、4行目に0~2秒のランダムなsleepが入っているのは、DBから合計値を取得するタイミングと、新しい計算結果をDBに登録するまでの間に強制的にタイムラグを発生させる(処理に時間がかかった場合に、不整合が起こるようなシチュエーションを再現させる)ためだ。

カウントするAPIだけだと状況がわかりにくいんで、現在DBに登録されている内容を表示するAPIも追加しておこう。

Route::get('list', function () {
   $list = Counter::orderBy('id')->get();
   $result = '';
   $total = 0;
   foreach ($list as $row) {
       $result .= $total . '+' . $row->value . '=' . $row->total . "<br />\n";
       $total = $row->total;
   }
   return $result;
});

これでhttp://[sample_app_host]/listにアクセスすると、現在DBに登録されている内容が以下のように表示される。

0+4=4
4+2=6
6+1=7
7+5=12
12+2=14
14+5=19

ちなみに上記の内容は、カウントAPIをブラウザごしに何回かリロードした結果となっている。しかし、実際にはWeb APIは一つのブラウザから順序よくアクセスされるとは限らない。そこで、いったんcountersテーブルをリセットし、apache benchを使って、以下のような複数同時アクセスを発生させてみよう。

ab.exe -c 5 -n 20 http://[sample_app_host]/count

その結果は以下のようになった。

0+1=1
1+1=2
2+3=4
4+2=3
3+5=6
6+1=13
13+5=18
18+3=4
4+2=20
20+3=26
26+5=26
26+4=25
25+3=38
38+3=24
24+2=23
23+3=46
46+2=28
28+3=38
38+2=43
43+2=43

集計処理とその結果をDBに登録する処理の間にランダムなタイムラグ(sleep)があるため、正しい結果になっていない。

そこで、キューを使って、呼び出し&登録処理が非同期でも、実際の計算処理は必ず順番通りに行われるようにしてみる。

Route::get('qcount/{value?}', function ($value = null) {
    Queue::push(function ($job) use ($value) {
        $value = is_null($value) ? rand(1, 5) : $value;
        $total = Counter::sum('value') + $value;
        sleep(rand(0, 2));
        $counter = Counter::create(array('value' => $value, 'total' => $total));
        $counter->save();
        $job->delete();
    });
    return 'Queued.';
})->where(array('id' => '\d+'));

http://[sample_app_host]/qcountにアクセスすると、先ほどcountで実行した内容と同じ処理をクロージャとしてキューに登録するように書き直した。キューに登録した時点では処理は行われないので、この時点ではDBに登録処理は行われない。

php artisan queue:listen

を実行すると、キューに登録された処理を順次実行していくことになる。

はずだったのだが、実際には上記コードはうまく動かなかった。listenでキューの内容を順次実行するのではなく、

php artisan queue:work

を実行して、最初の一件のジョブだけを実行してみると、以下のようなエラーが発生していた。

{"error":{"type":"Symfony\\Component\\Debug\\Exception\\FatalErrorException","message":"syntax error, unexpected ';'","file":"\/path\/to\/app\/vendor\/laravel\/framework\/src\/Illuminate\/Support\/SerializableClosure.php(173) : eval()'d code","line":12}}

クロージャをevalする際に構文エラーが発生しているようだが、深追いする気になれなかったので、クロージャを使う方法はあきらめた。

クロージャを使わない方法としては、キュージョブを実行するクラスを用意し、そのクラス名を登録する方法がある。app/libs/QueueTest.phpとして、

<?php

class QueueTest
{
    public function fire($job, $data)
    {
        $value = is_null($data['value']) ? rand(1, 5) : $data['value'];
        $total = Counter::sum('value') + $value;
        sleep(rand(0, 2));
        $counter = Counter::create(array('value' => $value, 'total' => $total));
        $counter->save();
        $job->delete();
    }
}

を用意し、QueueTestクラスをオートロードするために、app/start/global.phpのClassLoader::addDirectoriesに、「app_path().'/libs'」ディレクトリを追加しておく。

さらにキューを使ったルートの内容を以下のように変更する。

Route::get('qcount/{value?}', function ($value = null) {
    Queue::push('QueueTest', array('value' => $value));
    return 'Queued.';
})->where(array('id' => '\d+'));

これで、キューに登録されたジョブを処理する際には、QueueTest::fire()メソッドが呼ばれるようになる。apache benchを使ってこちらの実行結果も試してみると、

ab.exe -c 5 -n 20 http://[sample_app_host]/qcount
0+1=1
1+4=5
5+5=10
10+2=12
12+5=17
17+3=20
20+2=22
22+2=24
24+3=27
27+2=29
29+2=31
31+1=32
32+4=36
36+2=38
38+4=42
42+5=47
47+3=50
50+4=54
54+2=56
56+2=58

こちらはAPIは同時アクセスで呼び出されても、処理自体は順次(シリアルに)実行されているのがわかる。

というわけで、Laravel 4のキューを実際に使ってみたら、なんかうまく動かない部分もあったけれども、一応回避して使えることはちゃんと使えました、よかったよかった、という話でした。

※すでにクロージャを使ったキュージョブが登録されている場合は、クラスを使ったキュー登録処理を実行する前に、beanstalkdを再起動するなどして、クロージャを使った(=実行できない)ジョブをクリアしておく必要がある。

※キューをリッスンしておくために、あらかじめsupervisordなどを使って、php artisan queue:listenを自動起動&自動再起動するように設定しておくと便利。

追記)クロージャでジョブを登録するとSyntax Errorが出る件は、

    Queue::push(function ($job) use ($value) {

    Queue::push(function($job) use ($value) {

のように、クロージャの「function」と「(」の間の半角スペースを除去すれば動くようになった。see: SerializableClosure::getCodeFromFile()。

現状のLaravel 4の仕様を厳密に捉えると、バグといえばバグなんだろうけど、修正するよりもPSR-2に準拠して書こうってことで、使う側が対処する方が真っ当な気もする。strposの代わりにpreg_matchを使ってクロージャの開始位置を探すようにすれば修正は可能だけど。

Published At2013-07-05 20:01Updated At2020-01-01 12:58

技術日記
CSRFについてかみ砕いて説明してみるEdit

CSRFとか、わかりにくい言葉をわかりにくいままに使っているから、いまいちちゃんと知識が広まらないし、対策もひろまらないんじゃないか。

CSRFを「よそのサイトの機能を呼び出す攻撃」とかひらいて表記するようにすれば、もうちょっと雰囲気が伝わるんじゃなかろうか。

よそのサイトの機能ってのは、ごく普通のサイトだったら「メール送信フォーム」とか「掲示板」とか「コメント投稿」とか、そんなもののこと。

高機能なサイト、たとえばmixiとかFacebookとかのSNSなんかだと、友達登録とかメッセージとかいろんな機能がある。

そういう機能は、サーバー上で動作するプログラムによって動いているんだけど、サーバー上のプログラムに指示を出すのは、Webブラウザからのリクエスト。

Webブラウザからサーバーにリクエストを出す一番簡単な方法は、URLを呼び出すこと。

ブラウザのURL欄にごちゃごちゃ書いてリターンキーを押すと、そのURLによって指定されたサーバーに、リクエストが送られる。

一番よく使われるリクエストは、そのURLにあるコンテンツ(テキスト、画像、ムービー)をくれ、というリクエスト。普通にWebページを見るために使われる機能ね。

ごくごく単純な場合は、サーバーはリクエストされたURLに対応するサーバー上のファイル(テキスト、画像、ムービー)をそのまま返してくれる。

ただし、単にURLを呼び出すだけでも、サーバー上では何らかの特別な機能を実行する場合もある。

たとえば、

http://example.com/mail_send.php?subject=test&amp;to=someone@example.com&amp;body=hello

みたいなURLを開くだけで、「someone@example.com」というメールアドレスに、「test」というタイトルで本文が「hello」というメールを送信するプログラムなんてものもかんたんに作れる。

セキュリティってもの全く考慮せず、「自由に宛先とかタイトルとか本文とかを指定してメールを送るプログラムをサイト上に作れ」とか言われたら、そういうものができあがる。

もちろんそれに合わせて、subject、to、bodyを入力するフォームもサイト上には用意されるだろうけれども、入力フォームは、メール送信機能そのものではなく、メール送信機能に渡すパラメータを決めているだけだ。

入力フォームなんてものがなくても、上記のURLを開けばそれだけでメールは送信されるし、パラメータの内容はフォームがなくてもかんたんに変更できる。

そんなのがそこらに放置していると、上のURLの中のsubject、to、bodyのところを書き換えれば、自分の好きなところに好きな内容のメールが送れてしまうことになる。

じゃあ、そうやって自分が送りたいメールの内容に書き換えたURLを、試しにそこらの掲示板にでも貼り付けてみよう。「すごくおもしろいゲームを見つけたよ!」とかコメントを添えて。

そのURLをクリックする人がいれば、その数だけメールが送信されてしまうことになる。

もちろんそのURLを呼び出した=メール送信機能を実行したIPアドレスは、そのリンクをクリックした人間のものになる。

ただし、上記のようなURLが直接ブラウザで開かれた場合、その結果としてはメール送信プラグラムの実行結果ページが表示されるので、メール送信が実行されたことに気づくことができるだろう。

しかしそれをごまかす方法はいくつもある。

URLを開く方法として、普通の人が普通に知っているのは、リンクをクリックしたり、Webブラウザのアドレス欄にURLを書くことだろうけれども、それ以外にもいろいろな方法がある。

たとえば、HTMLを知っている人ならば、<img src="http://example.com/img/picture.jpg"&gt;というHTMLを書けば、http://example.com/img/picture.jpgに置かれている画像が表示されることは知っているだろう

これは、WebブラウザのURL欄に書くのと同様に、http://example.com/img/picture.jpgを呼び出している。

つまりHTML中に、<img src="http://example.com/mail_send.php?subject=test&amp;to=someone@example.com&amp;body=hello"&gt;と書けば、それが書かれたHTMLを開くと、自動的にメールが送信されることになる。

でも、そんな風に書いたら画像が呼び出されくて、画像が表示されないエラーが表示されるから、すぐにわかるよね、と思う人もいるかもしれない。

じゃあ、<img src="http://example.com/mail_send.php?subject=test&amp;to=someone@example.com&amp;body=hello" style="display:none;">とかかれていたらどうだろう。

最後の「display: none;」というのは「表示しないでね」という指定だ。これならば、画像読み込みのエラーが表示されることはない。

それ以外にも外部のURLを自動的に開くHTML表現はたくさんあるので、imgタグのところだけチェックすれば大丈夫だね、ということにはならない。

ところで、今まで説明してきたみたいに単純にURLを開くだけのことをGETリクエストという。

フォームからいろいろなパラメータを指定してサーバーに送信する際に、GETリクエストを利用すると、先ほどから書いているように「subject=test&to=someone@example.com&body=hello」という「項目名=値」を「&」でつないだようなURLの形でリクエストが行われる。

HTMLで書くと

<form action="http://example.com/mail_send.php"> タイトル <input type="text" name="subject"> 宛先 <input type="text" name="to"> 本文<textarea name="body"></textarea> <input type="submit" value="送信"> </form>

みたいな フォームを用意しておくと、上記のようなGETリクエストが送られることになる。

一方、URLにパラメータが含まれないPOSTリクエストを指定したフォーム送信の方法もある。上記のHTMLの一行目を、

<form action="http://example.com/mail_send.php" method="post">

に変えると、フォーム送信でアクセスされるURLは、

http://example.com/mail_send.php

のままになり、先ほどのような「subject=test&to=someone@example.com&body=hello」という部分はURLにはつかない。

じゃあパラメータはどうやって送られているのかというと、URLの一部ではなく、普通にWebブラウザを使っているだけの人間の目には触れない別の形で、サーバーに送られる。

サーバーでは、GETリクエストで送られたパラメータとPOSTリクエストで送られたパラメータは識別できる。

だから、http://example.com/mail_send.phpで動いているプログラムが、POSTリクエストでパラメータが送られた場合にしか動かないように作られていれば、単に

http://example.com/mail_send.php?subject=test&amp;to=someone@example.com&amp;body=hello

みたいなURLを呼び出しただけでは、メール送信機能が動いてしまうことはない。

じゃあ、サーバー上のプログラムを、POSTリクエストのみで動くようにしておけば、勝手に使われることはないんじゃないの?と思うかもしれないが、そうではない。

たとえば、

<form action="http://example.com/mail_send.php" method="post"> <input type="hidden" name="subject" value="test"> <input type="hidden" name="to" value="someone@example.com> <input type="hidden" name="body" value="hello"> <input type="submit" value="このボタンを押すと面白いよ!"> </form>

みたいなフォームをどこかの無料ホームページサービスなんかに置いておくとしよう。

上のフォームの「type="hidden"」というパラメータは、Webブラウザ上には見えない形でパラメータを指定する方法なので、上のフォームを表示すると「このボタンを押すと面白いよ!」というボタン一つが表示されるだけだ。

このボタンを見ただけで、いったい何が起こるのかがわかる人はいないだろう。

「何か面白いことが起こるのかも」と期待してボタンをクリックしたら、メールが送信されてしまう。

これだとユーザーがボタンを押さなければメールが送信されないので、ボタンを押さなければ安全なのね、と思うかもしれない。

しかし、それを回避して自動的にPOSTリクエストのフォームを送信する方法もある。

<script> var forms = document.getElementsByTagName('form'); forms[0].submit(); </script>

というJavaScriptは、HTML中に含まれるフォームの最初の一つを自動的に送信する機能を持つ。

つまり、フォームのHTMLと上記のようなJavaScriptを書いておけば、ページを開いただけで自動的にフォームが送信されてしまうことになる。

ただし、これはJavaScriptが有効な場合のみ働くので、WebブラウザでJavaScriptが動作しないように設定しておけば、この攻撃は回避できる。

一昔前までは、「信用できるサイト以外ではJavaScriptはオフにしよう」なんて使い方をすることで、こういう攻撃は回避できていたのだけれども、今時はもうJavaScriptが当たり前のように使われすぎているんで、そんな面倒な使い方は現実的ではないだろう。

しかし、このような方法でフォームを自動送信した場合は、Webブラウザはメール送信結果ページを表示するので、メールが送信されたことには気づくことができるかもしれない。

しかしそれをごまかす方法もある。

たとえば、フォームの1行目を、

<form action="http://example.com/mail_send.php" method="post" target="test">

のように「target="test"」を指定する。そして、

<iframe name="test" style="display:none;"></iframe>

のように見えない(display: none;)インラインフレーム(iframe)を用意しておく。

インラインフレームというのは、Webページ(HTML)中に別のWebページを埋め込むための仕組みだ。

自動的に送信されたフォームの結果は、インラインフレーム「test」に表示されるのだが、インラインフレーム「test」は表示しない設定になっているので、そのページを開いた人にはメール送信結果ページが見えることはない。

このような仕組みの罠は、HTMLという形で記述する必要があるので、単純に攻撃URLを掲示板に貼り付けることはできない。

こういう罠を仕掛けたHTMLをどこかのサーバー上に置いておき、そのURLを掲示板なんかに貼り付けるという2段階の仕組みが必要になる。

ちゃんとしているサーバ運営会社ならば、そういう危険なHTMLを仕掛けてもすぐに撤去されるし、罠を仕掛けた人間をアクセスログなどから追跡することにも協力的だろう。

というわけで、そういう罠が仕掛けられているページは、海外のあまり日本ではメジャーではないサーバーなどを利用して設定されていたりする。

じゃあ、知らないURLっぽいページは開かなければいいよね、と思うかもしれない。

しかし、今時は長いURLを別の短いURLに変換してくれるサービスなんかもあり、そういうものが使われていると、実際にクリックしてみるまで、どういうURLが開かれるのかはわからない。

また、実際にクリックしてみたところで、人間の目には実際にどういうURLが開かれたのかわからない場合もある。

Webブラウザは、リダイレクトという機能に対応している。リダイレクトというのは、指定されたURLから別のURLに自動的に移動する機能だ。

サーバーにリダイレクト先URLを指定した結果を返されたら、ブラウザは自動的にそのURLに移動する。リダイレクトは一瞬で行われるので、ユーザーの目にはリダイレクトされたかどうかは識別できない。

だから、ある短縮URLを開いた結果、たとえばhttp://www.yahoo.co.jp/が表示されたから、安全なURLだった、とは限らない。

たとえば、http://example.co.jp/too_dangerous_page/という罠が仕組まれたページに移動した後に、なにやら危険な処理を実行し、その後すぐにhttp://www.yahoo.co.jp/に移動したのかもしれない。

ユーザーの目に見えるレベルのURLなんてものは、かんたんにごまかすことができる。

これは偽装というレベルではなく、インターネットにおける当たり前の技術を当たり前に使っているだけのことだ。

というわけで、ここまで説明したいたちごっこで、ユーザーができる比較的安全な対処方法は、JavaScriptをオフにすることくらいなのだが、今となってはもうそういう訳にはいかないだろう。

ということで、ユーザー側が自衛できない以上は、サーバーサイドで対処しなければ、こういう攻撃は回避できない。

サーバーサイドでの対処策に関しては、技術者だけがわかればいい話なので、ここでかみ砕いて説明する必要はないだろう。方法論は確立しているので当たり前に対応すればいい。

セキュリティ的な危険性がある機能に対して回避策を講じていないのは単なる手抜きなので、非難されてしかるべきだろう。

ただ、認証不要のサイトにおける送信先固定のメールフォームに、CSRF対策が必須かというと、今回の事件が起きるまでは、「これはなりすましが可能ですよ」と関係者が理解していれば、CSRF対策をしていなくてもいいと思っていた。

が、今回の事件で「これはなりすましが可能ですよ」という理解を関係者に求めることは難しいように感じたので、今後は認証不要なサイトのメールフォームレベルの機能でも、CSRF対策は必要だろう。

また、このような知識はもうずいぶん前からインターネット技術者には当たり前のように知られていた内容なので、このような知識を前提とせずにインターネット犯罪を捜査しちゃまずい。

今回の事件は、単なるCSRFとウイルス経由の遠隔操作が混ざっているっぽいんだけど、前者は十分にあり得ることであると理解できていなかったとしたら、まずい。

その辺の情報がはっきりしないんだけど、どうなんだろう?

掲示板経由で命令を出すウイルスに関しては、botネットなんかではよく使われていると言われている手法だから、インターネット犯罪を捜査する人間ならば、方法的には知っていてしかるべきではある。

けれども、この辺はセキュリティを本格的に研究している人しか、具体的な情報に触れることはないかもしれないから、詳しく知らなくてもしょうがないかもしれない。

というか、私もそういうものらしいと噂でしか聞いていなかったから、実際にこういう事件があったときにそういう可能性を主要な選択肢としては思いつかなかったかもなー。

なんかものすごく長くなった。かみ砕いて説明したつもりだったけど、あんまりかみ砕けてないかもしれない。

[宣伝] Twitterベースの掲示板システム開発中! http://tweet-bbs.com/

Published At2012-10-23 20:23Updated At2012-10-23 20:23

テニス観戦記
今の4強の特徴をうまく説明してくれEdit

今の4強の特徴をうまく説明してくれをやってみる。

  • フェデラー: 強力なフォアハンドと球種とコースが絶妙なサーブを中心に、自らポイントをもぎ取る能力が高い超攻撃的オールラウンドプレイヤー。圧倒的な実績に裏付けられた強いメンタル。体に負担をかけない効率のいいボディコントロール。片手バックハンドの構造的な弱点(高い打点)だけはどうしても完全には克服できないが、そこをつける敵は限られている。30歳を過ぎてフィジカルは衰え気味。
  • ナダル: 強力なフィジカル(筋力)を背景に、オープンコートに打ち込まれた球でもコースが読めていれば追いつける驚異的なフットワークと、ミス(バックアウト)せず強打し続けることができる高く跳ねるヘビースピンストローク。無理に攻撃しなくても、そのストロークのパワーとコートカバーリングで相手を圧倒できることが多い。そのディフェンス力の高さから、球足の遅いコート(特にクレー)では圧倒的に強い。フィジカルの負担が膝の故障につながりがち。
  • ジョコビッチ: ディフェンシブなプレーを得意としながらも、自ら攻撃していく頻度も高いストローカー。ベースラインストロークは攻防ともに非常に高い能力を持つ。サーブも多少不安定なところはあるが強力。ネットプレイ(+ドロップショット)なども実戦でトライすることが多く、そのレベルを徐々に高めつつある。グルテン(小麦粉)アレルギー発覚以降の食事制限によってフィジカル的な問題(呼吸器障害)を克服した。集中するポイントではサーブ前の玉突きが長くなりがち。
  • マレー: 恵まれたフィジカルとテニスセンスを持つ才能あふれるプレイヤー。あらゆるショットに高い能力を持ち、特に長身から打ち下ろすビッグサーブとフットワークを活かしたカウンターショットが優れている。母国イギリスでの期待が大きくそのプレッシャーがあるためか、メンタル面の揺らぎが見え隠れすることも多く、それがプレーの不安定さにつながることがある。初グランドスラム(全米)制覇でメンタルが安定すれば1位も狙える。
もっと短くまとめたかったけど、いまいち長くなっちゃうな。

Published At2012-09-12 18:57Updated At2012-09-12 18:57

技術日記
iPhoneアプリ『QQ81』作りましたEdit

2012/08/29追記 現在公開されているバイナリが起動後すぐに落ちてしまうようです。開発環境では実機動作しているんですが…。原因はよくわかりませんが、ひとまず動作していた一つ前のバージョン相当に戻したバージョンを公開申請に出しました。おそらくそちらが公開されれば動作するようになると思います。動作しないバージョンをダウンロードした方、申し訳ありませんでした。


iPhoneアプリ『QQ81』作りました。暗算アクションパズルです。

画面下の5×5のパネル部分にランダムに数字が現れてくるので、それをかけ算(九九)が成立する順番にタップして消していきます。

素早く連続して消したり、同じ数字同士のかけ算で消したり、パネル上のすべての数字を消すとボーナス。

かけ算が間違っていると数字が増殖し、パネルのすべてが数字で埋まるとゲームオーバーです。

本当はちょっと前に公開したんですが、iOS4で動かないとかいろいろバグがあったのがようやく直った(というか、直した版がようやくAppStoreに載った)のでここでも紹介します。

ちなみに無料アプリです。一応広告も貼ってあるけれども、起動時にしか表示しないトップページにしか貼ってないんで、おそらくタップされることはほぼないでしょう。

で、このゲームはMonoTouchで初めてちゃんと作ったアプリだったんですが、やっぱりMonoTouchというかC#はいいねー。Objective-Cでも書けなくはないけど、主に可読性的な面でフラストレーションがたまる。

その点、C#だとほぼ考えたとおりにコーディングできるんで、非常に書きやすい。オブジェクトの生存管理もふつうにガベージコレクション任せに出来るし。

ただ、基本的なロジック部分はいいんだけど、iOSの機能を使おうとすると、ほぼC#環境に直訳されただけの、Cocoaフレームワークやその他iOS用ライブラリ群を呼び出すことになる。

だから、「MonoTouchがあればObjective-Cなんてわからなくても、iPhoneアプリがすいすい書けるぜ!」なんてことにはならない。iOS用ライブラリの使い方を調べようと思ったら、Objective-Cで書かれたサンプルとか読めないと困るし。

それでもまあ俺的には、.hファイルと.mファイルを行き来しなくても良くなったり、

[sourcecode language="objc"] MyViewController *contrller = [[MyViewController alloc] initWithNibName: @"MyViewController" bundle: nil]; [self presentModalViewController: controller animated: YES]; [controller release]; [/sourcecode]

みたいなコードを、

[sourcecode languag="csharp"] var controller = new MyViewController(); PresentModalViewController(controller, true); [/sourcecode]

なんて感じに書けるだけでもだいぶ心に平穏が訪れる。ただ、

[sourcecode language="objc"] [UIView beginAnimations: nil context: nil]; [UIView setAnimationDuration: 0.2f]; [UIView commitAnimations]; [/sourcecode]

みたいなほぼAPI呼び出しだけやるようなコードだと、MonoTouchにしたところで、

[sourcecode language="csharp"] UIView.BeginAnimations(null, null); UIView.SetAnimationDuration(0.2f); UIView.CommitAnimations(); [/sourcecode]

になるだけなんで、あんまりC#って感じもしなくなってくる。

特にiOSではまだまだ現役で古いC API(非Objective-C API)とかが残っているし、そういうのもべた移植なんで、その辺使うとだいぶ「なんだかなー」という気分になれる。

ちなみに、非標準開発環境であるMonoTouchだからこそはまった、というようなことは今回はほとんどなかった。

MonoTouchで開発中にググって見つかる関連情報はかなり少ない(特に日本語は)んだけど、互換開発環境としては結構出来がいいのか、あんまり変なはまり方はしないし、Objective-CとC#(MonoTouch)の読み替えが出来れば、Objective-C向けに書かれた情報で解決することがほとんど。

iOSの機能もほとんどもれなく移植されているし、一応自前で移植されていないAPIなんかをC#から呼び出す方法も用意されている。ただ、その辺の情報はググって草の根情報を見つけるよりも、xarmin(MonoTouchの開発元ね)のドキュメントを読み込んだ方が最終的にはわかりやすいかも。

そういえば、MonoTouchの開発環境であるMonoDevelopのMac版には、日本語入力が出来ないというかなりアレな欠陥がある。

ソースコード中に日本語を入力したければ、他のエディタとかで入力した日本語テキストをコピペしたり、そのファイルだけ別エディタで開いて編集したりする必要がある。

まあゲームプログラムなんてあんまり日本語を使うことはないし、それほど関係ないかなーと思っていたんだけど、MonoDevelopからcommitするときのコミットログでも日本語が書けないのが、さりげなく面倒くさかった。

ちなみにこの欠陥はGUIツールキット周りの不具合で、だいぶ前から直っていないようなんで、今後もすぐに直ることはなさそうだ。

Objective-C用に用意されたサードパーティライブラリをC#で使う方法とか、他の環境向けのC#用ライブラリをMonoTouchで使う方法とかは、結構はまりやすそうな話題なんだけど、今回はほとんどその辺をやることがなかったんで、今後の課題かな。

というわけで、iOS用アプリ開発環境としてのMonoTouchはかなり気に入ったんで、今後のますますの発展をお祈りしたいところなんだけど、ググって見つかる日本語情報の少なさ=今後ますますご発展する可能性の少なさ、という気がするんで、少しでもMonoTouchが流行ってくれることを祈っていろいろ書いてみた。

できればMono自体がもっと流行って、サーバーサイドアプリとかもMonoで書ける世の中が来るといいんだけどな-(今でも一応書けるけど、それで商売する勇気がある人は少ないよね? MS純正環境じゃなくて、mod_monoとかのほうね)。

Published At2012-08-24 18:42Updated At2012-08-24 18:42

日記
テニスシミュレーションゲームを考える その3Edit

その後、いろいろ複雑なことを考えて、テストコードを書いてみたりしたんだけど、なんかうまく収束する方向に持っていけない。

基本的なアプローチとして、実際に自分がテニスをやるときに考えていることや、行動していることをパラメータに置き換えながら何とかしようとした場合、あまりにもパラメータとシチュエーションが多くなりすぎて、どうしても発散してしまうようだ。

そこで、そういう実際にプレイ中のその場その場で出てくる要素をパラメータ化するアプローチはあきらめ、実際のプロテニスプレイヤーをどの程度パラメタライズしたら、そのキャラクターというものが十分に表現できるのか、というアプローチで考え直してみた。

今のところ考えているのは、次のようなパラメータ。

  • サーブ(Serve)
  • レシーブ(Recieve)
  • フットワーク(Quickness)
  • フォアハンドストローク(Forehand)
  • バックハンドストローク(Backhand)
  • ネットプレイ(Net)
  • ショットセレクション(Tactics)
  • フィジカルタフネス(Physical)
  • メンタルタフネス(Mental)
  • 安定感(Consistency)
  • ハードコート適性(Hard)
  • クレイコート適性(Cray)
  • グラスコート適性(Grass)
このパラメータで試しに主だったプレイヤーをSABCDEFでランク付けしてみると、
名前 S R Q F B N T P M C H C G
ふぇで S B A S B B S A S S S A S
なだ B B S S A C C S S S A S S
じょこ A A A A A C A B A A S A A
まれ A A S B A B A A B B S B A
ふぇれ B A A B B C C S A A A A B
ろで S C C A C B C B B C A C A
つぉん A B B A C A B B B B A C A
こり C B A A A D A C B C A B B
でんこ B B B B B D B B B C A A D
でるぽ A B B A A C C B A C A A B
そえだ B C C C C C C C C C C D C
かるろ S C C C C A A C B B A D S
なんて感じ。異論は認める。というか、やっているうちにだるくなってきて後半だいぶ適当になっちゃったな。

でも、このくらいあればだいぶ選手の個性を表現できているように思える。あとはこのパラメータを使って、スタティスティクスが出せる程度のシミュレーションができるかどうかだ。

ちなみに俺が出したいスタッツは、ツアーレベルのやつじゃなくて、GSレベルのやつ。具体的には、

  • ファーストサービス確率
  • ファーストサーブポイント獲得率
  • セカンドサーブポイント獲得率
  • サービスエース数
  • ダブルフォルト数
  • ネットプレイ成功率
  • ウィナー数
  • アンフォーストエラー数
  • ブレイク数/ブレイクポイント数
あたり。あと、
  • プレイヤーごとの対戦成績
  • コートサーフェイスごとの成績
なんてあたりも生涯成績として表示できるようにしたい。というか、基本は数値シミュレーションなんで、その程度の細かい情報が出てこないと面白くない。

なんか話が逸れつつあるけど、ひとまずこんな感じのパラメータ表現で、どうやって細かいスタッツが出せる程度のシミュレーションを行うか。そのシミュレーション結果がリアルなスタッツと同じ感じになるように調整できるか。ってあたりをサンプルコードを書きながら調整していきたい。

まずは数値計算上試合が成立するところまでサンプルを作ってみよう。

Published At2012-03-28 19:37Updated At2012-03-28 19:37

日記
テニスシミュレーションゲームを考える その2Edit

どういう内容のシミュレーションを行うかを考えるにあたって、まずは実際のテニスのプレイ中に取る行動や考える内容について、できるだけ細かく分解して考えてみる。

まずはサーバーがサーブを打つ前の段階。

  • センターもしくはワイドぎりぎりに、エースを狙って打つ(入れば優位を取れる)
  • 相手の読みを外すコース(センター、ワイド、ボディ)・球種(速さ、スピン量の調整)で打つ(威力ではなくプレースメントで優位を取ろうとする)
  • 相手の苦手なサイドや球種で入れにいく(ある程度読まれていても不利な展開になりにくい)
あたりが普通の選択肢か。

一方レシーバーサイドがサーブを待っている状態で。

  • エース級のサーブを警戒し、予測と賭けで何とかレシーブしようとする
  • 手の届く範囲の球を、安定して深くに返そうとする
  • 手の届く範囲の球は、強引に叩いていこうとする
  • ポジションを上げて、相手のサーブを早いタイミングで叩いていこうとする
  • コースを読んで(回り込みなども行い)、読みが当たった場合はレシーブを叩いていこうとする
とかかなー。

これはまだボールが動く前の段階での、プレイヤーの頭の中での選択肢。

続いて、実際にサーバーがサーブを打つ。

  • 狙った球種とコースで、サーバーの能力のn%の速度や回転を持つサーブが入る
  • 狙った球種だがコースがずれ、 サーバーの能力のn%の速度や回転を持つサーブが入る
  • フォルトになる
サーブの結果としてはこんな感じかな。ポイントとしては、
  • 球種はサーバーが決め、それはその時点で確定する。
  • コースは、狙い自体はサーバーが決めるが、ある程度のぶれは出てくる。また、厳しいコースを狙った場合は、ブレ=フォルトになる可能性が高い。
  • 速度や回転(切れ)は元々のサーバーの能力以上の結果はあり得ず、どのくらいベストに近い球を打てるか、という問題になる。
  • 当たり損ね、フォルトの種類(ネット、アウト)、レットによる打ち直しなんかは省略。
なんて感じ。

そのサーブに対するレシーバー。

  • 読み or 反応 or フットワークが悪くて触れない(サービスエース)
  • ぎりぎり追いついたがきちんとラケット面に当てられずにリターンが返らない
  • ぎりぎり追いついたがスライス面で返すだけ
  • 追いついたが苦しい体勢でリターン
  • 追いついてフォアハンド or バックハンドで深くにリターン
  • 追いついてフォアハンド or バックハンドで(ネットダッシュしてきた相手の)足下にリターン
  • 追いついてフォアハンド or バックハンドで(ネットダッシュしてきた相手の)パッシングを狙う
  • 予測が当たって攻撃的にリターン
このくらいのバリエーションでいいかなー。ポイントとしては、
  • 読み+反応+フットワークでレシーブポジションに入れるかどうかが決まる。
  • レシーブポジションへの入り方によって、レシーブの打ち方に制限が出る。
  • ブロックリターン能力が高い場合は、ぎりぎり届いた場合もリターンを深くに押し戻すことができる。ブロックリターン能力が低いとリターンミスもしくは相手のチャンスボールになる。
  • レシーブポジションへの入り方に余裕があるほど、レシーバーは攻撃的なリターンを選択することが可能になる
  • 相手がベースラインにいる場合と、ネットダッシュしている場合とでは、レシーバーの行動選択肢が変わる。
  • 事前の戦術選択肢+実際にボールを打つ瞬間の状況によって、実際にボールを打つ瞬間の選択肢が決まってくる
  • リターンも通常のストロークとショットバリエーションは基本的に変わらないが、ストロークとは別系統の、読み+反応能力が必要。
  • サーブは、通常のストロークよりも強力なショットになりがちなので、レシーブは通常のストロークよりも、ポジションに入ることが難しいことが多い。
なんて感じか。

サーブとレシーブを考えるだけでも、だいぶややこしくなってきたなー。

ストローク戦の方がもっとバリエーションが多いだろうから、ひとまずこのサーブとレシーブの考え方を下敷きに、ストローク戦に対するアプローチも考えていこう。

現状でサーブとレシーブに関して必要そうなパラメータを考えてみる。まずはサーブ。

  • サーブの読みにくさ
  • サーブの威力(主にフラットサーブのスピード)
  • エース狙いのサーブのコースの厳しさ
  • エース狙いのサーブの確率
  • 入れにいくサーブの威力(セカンドにしては強いサーブが打てるか)
  • 入れにいくサーブの打ちにくさ(読まれても叩きにくいサーブが打てるか)
  • 入れにいくサーブの確率(これはいらないかなー。入れにいくサーブが入らないレベルまで考える必要はないだろう)
  • 状況(重要なポイントなど)におけるメンタルへの影響
続いてレシーブ。
  • サーブに対する読みの良さ
  • レシーブ時の反応の良さ
  • ブロックリターン能力(苦しい状態)
  • 基本的なリターン能力(通常)
  • 攻撃的なリターン能力(叩きにいったとき)
  • 相手のネットダッシュに対する反応
  • フォアハンドリターン能力
  • バックハンドリターン能力
  • 状況(重要なポイントなど)におけるメンタルへの影響
とひとまず考えてみたけれども、通常のストロークをどう扱うかを考えてからじゃないと、レシーブの扱い方は決めようがないな。レシーブはストロークの延長だからなー。

あと、今考えているのは絶対的な能力の高低を数値で表すイメージなんだけど、実際のテニスでは相手との相対評価の方が大きい場合もある。いわゆる相性の悪い相手の問題。

こういうのは、単なる数値の大小だけでなく、特定の能力を持つ相手に対する強さ・弱さを表現する仕組みを用意しないと、表現しきれないだろうか。工夫すれば、数値の大小だけで表現しきれるか?

Published At2012-03-09 17:53Updated At2012-03-09 17:53