DEN将棋サービスについて

※「DEN将棋EX」リリースにともない、DEN将棋・DEN将棋Xは閉鎖しました。

 下手な将棋の相手をしてくれる人が近くにおらず、スマートフォンを使ってひたすら「ぴよ将棋」を指していたのですが、スマートフォンを使って友人同士で手軽に将棋を指せないかと調べてみると、あまりピンとくるアプリが無かったので、自作しました。

 友人に強制的にデバッグに付き合ってもらったおかげで、結構自信作が完成!対局後そのまま棋譜並べモードに入り、自由に棋譜を分岐でき、棋譜保存/読み出し機能と併用すれば対局後の研究用としても結構使えます。また、JKF形式←→KIF形式の相互ファイルコンバータも作りましたので、将棋DB2 のような棋譜ダウンロードサービスから落としたKIF形式のプロの棋譜をJKFに変換して再生できますし、対局後に保存したJKF形式ファイルをKIF形式に変換すればスマートフォン上のぴよ将棋で自動解析させることもできます。

 ネット対局専用なのでAIによる思考エンジンとかそういう高度なコーディングをしたわけではありません。使い方は「ココ」(実際の対局は「こちら」)に記載しましので、ここでは実際のソフトウェアの概要について記事にしておきます。詳細は書ききれないのでここではあくまで概要に留めていますが、使用しているテクニックなどを今後取り上げて備忘録としてブログ上で記事にするかもしれません。

1.構成

denshogi_renkan.png

図1 

 図1はwww.webkoza.comで稼働しているソフトウェアの連関図です。色の付いている部分が今回作ったプログラムによって動作している部分です。対局としては下記の種類が可能です。(ブラウザ1やブラウザ2はPC・スマートフォン・タブレット何でも構いません)

  • DEN将棋 対 DEN将棋(ブラウザ1 対 ブラウザ2)
  • DEN将棋 対 将棋所 (ブラウザ1 対 将棋所1)
  • 将棋所1 対 将棋所2

 CSA(コンピューター将棋協会)プロトコルを搭載したshogi-serverが、将棋の対局決定・手番管理・反則判定・勝敗判定などを行っています。このソフトウェアはCSAが策定した独自のTCP Socketプロトコルによって対応クライアントと通信することにより、クライアント同士の対局を実現してくれる大変便利なソフトウェアです。

 shogi-serverを稼働してポートを開放しておけば、将棋所のようなshogi-serverとの通信対局に対応したPC用ソフトウェアを使ってネット対局を楽しむことができます。www.webkoza.comでも開放していますのでこの方法で対局することができます。しかし、CSAプロトコルはhttpでは無いので、スマートフォンの汎用ブラウザを使ってネット対局を行うには、プロトコル変換サーバーが必要になります。そこで今回作ったのは以下の3つの部分です。

  • PSGIによるhttpリクエスト受付用daemon
  • ワーカープロセス(CSA Client)
  • ブラウザサイドプログラム

 当初shogi-serverはwww.webkoza.com内には置かずに、誰でも使用できる「floodgate」を使用させていただくつもりで、ここを使用しながら開発を行いました。しかし評価中になぜか「同一IPアドレスからの異なるクライアント同士の対局では5分間ぐらいの無通信が有ると、片方への指し手データが送られなくなる」という、私にとってかなり謎の現象が有ったため、あきらめました。幸いshogi-sererと稼働に必要なrubyは、簡単にインストールできましたので、あっという間にwww.webkoza.comで稼働させることができました。ただ、rubyのバージョン少し古いんですよねえ(1.9.3p484)。推奨は「2.1 系統」と書いてあるんですが今のところ支障は有りませんので知らなかったことにして使っています。

 ここからは、上記3つのソフトウェアについて概要を記します。

2.PSGIによるhttpリクエスト受付用daemon

 一つのpsgiファイル(shogi.psgi)、3つのパッケージ(Login.pm,SendDa.pm,GetQUe.pm)を作りました。

 shogi.psgiはplackミドルウェアのbuilderブロックとmountキーワードを使った複数のリクエストにより連携して機能するpsgi将棋アプリケーションです。ログインリクエスト→Login.pm、データ送信リクエスト→SendDa.pm、データ受信リクエスト→GetQue.pmのオブジェクトを各々生成してto_appを呼び出す構成となっています。builderブロックでは、enableキーワードを使用して、共通のミドルウェアとしてPlack::Sessionミドルウェアをラップしました。これによりログインリクエスト・指し手送信リクエスト・受信リクエスト全てにおいて、同一ブラウザセッションの管理を行います。下記はbuilderブロックです。SoreではFileを指定し、セッションファイルの場所はmountしてあるUSBメモリにしています。StateにCookieを利用しsession_keyを定義しています。また、expireを指定しないでブラウザを閉じるとセッションcookieが無効となるようにしました。(***は伏せ字)

builder{
 enable 'Session',
  store => Plack::Session::Store::File->new(
   dir => '/media/usb***/***'
  ),
  state => Plack::Session::State::Cookie->new(
  session_key => '***'
   #expires => 30
   #起動時expiresは指定しないことにする
   # →Login時クエリーでexpires指定しない場合はブラウザセッション閉でセッション断
  );
 mount "/shogi_login"=>builder{
  enable "StackTrace";
  $ShogiLogin;
 };
 mount "/shogi_send_da"=>builder{
  enable "StackTrace";
  $ShogiSendDa;
 };
 mount "/shogi_get_que"=>builder{
  enable "StackTrace";
  $ShogiGetQue;
 };
};

 psgi将棋アプリケーションは、グローバル変数を使って席管理を行っており、CSAのユーザー名、パスワード文字列をログインオブジェクトが受け取って、空いている席を割り付けた後、席に対応したワーカープロセスにshogi-serverへログイン要求を送るよう指示します。ログインオブジェクトはshogi-serverからのログインOKをJSONデータでレスポンスします。ここでブラウザ側は相手決定待ちになり、定期的なshogi-serverからのデータ受信キュー要求ポーリングに入ります。2つのブラウザによるログインから相手決定までの正常系シーケンスを下記に示します。

denshogi_login_seq.png

  

 先手後手が決定(match make)されると、ブラウザ側は定期的なshogi-serverからのデータ受信キュー要求ポーリング(/shogi_get_queを行いながら、自分手番の時は指し手データの送信(/shogi_send_da)を行い、相手手番の時は相手指し手データの受信待ちを行います。

 SendDaオブジェクト・GetQueオブジェクトのto_appでは先頭で下記のようにsession継続確認を行っています。

my $request = $self->get_req();
my $session = Plack::Session->new($request->env);
if ($session->get('verified')) {
#セッション継続確認OKの処理
}else{
#セッション継続確認NGの処理:NGレスポンス用JSONデータ生成
}

 セッション以外に着席番号をログイン成功時にcookieとしてブラウザに保存しており、このブラウザから常に送信される着席番号cookieを使って、対応するワーカープロセスを特定してプロセス間通信を行っています。

 fedora上でのdaemon登録は、shogi.psgiをplackupで起動するserviceファイルを作り、/usr/lib/systemd/system/にコピーしたのち、

# systemctl enable ***.service

でdaemon登録しました。plackupの-pオプションで指定したポートには、proxy_moduleのProxyPassとProxyPassReverseを指定してフォワードしています。(下記 httpd.conf)

ProxyPass /(psgi将棋アプリのトップURL)/ http://localhost:***/
ProxyPassReverse /(psgi将棋アプリのトップURL)/ http://localhost:***/

3.ワーカープロセス(CSA Client)

 ワーカープロセスは、psgi将棋アプリケーション起動時に固定数の子プロセスとして起動され、ログインシーケンスでshogi-serverに接続・ログインし、終局時にshogi-serverからログオフ・切断します。接続中はshogi-serverからの受信データをキューに保持しており、ブラウザからのポーリングリクエストを受けたpsgiからの要求が有ると常に全てのキューデータを渡します。

 ワーカープロセスの役割をまとめると下記になります。

  • CSAプロトコルによるshogi-server(TCP Socket Server)とのTCP Socket通信
  • 同上通信のための接続~切断シーケンス管理
  • 親プロセスから受け取った指し手データのCSAプロトコルへの変換
  • shogi-serverからCSAプロトコルにより受信したデータを親プロセスからの要求により返信
  • 上記受信データをキューに溜める

 このプロセスも100% perlで書いており、慣れ親しんだ最も伝統的な手法で作りました。親プロセス起動時にIPC::Open2で生成された子プロセスは、親プロセスからの指示/返信はSTDIN/STDOUT、shogi-serverとの送受信データ通信はSocketを使用する無限ループです。どちらも4引数selectを使用してノンブロッキングな受信ループを使いながら、全体シーケンス管理を行っています。

 工夫した点としてはゾンビプロセス(ゾンビシート:いつのまにか誰も座ってない席。。なんか怖い)対策です。ブラウザはその性質上自由にいつでも閉じることができますが、この場合対局中に通信が一切行われなくなったCSAクライアントプロセスが残り続けることになります。いずれ時間切れでshogi-serverからTIME_UPを受信して切断されますが、その時は既にブラウザがいないため、このTIME_UPをpsgiが取り出す事ができません。そこでpsgi側では、誰かが定期ポーリングしてきた際および誰かがログインリクエストを送ってきた際、全ての席の最新通信時刻をチェックし、一定時間以上途絶えている席を見つけるとブラウザが閉じられたと判断し、その席番号に対応したCSAクライアントプロセスを強制的に再起動することにしています。「一定時間」は今のところ30秒に設定しています。

4.ブラウザサイドプログラム

 全て javascriptで作りました。苦手なブラウザサイドという先入観もあり、当初ここが最も大変だと予想していた通り、なかなか手ごわいものでした。しかし盤面ロジック・手番管理・駒毎の移動/打ち込み可能場所判定などは、「んとか将棋」さんのコードをベースに何とか完成できました。

 独自に追加・変更した機能は下記です。

  • 通信対局専用にした
  • ajaxを使ったpsgi将棋アプリケーションへのリクエスト/JSON受信
  • 対局中の自分陣地を常に下側に固定
    これにより先手が上と下どちらもあり得るロジックに変更
  • CSA形式の指し手データから、内部駒データ・内部盤面座標への変換
  • 棋譜データの保持方法の変更
  • 棋譜データの内部形式からJKFへの相互変換
  • 棋譜データの保存(JKF)/読み出し
  • 棋譜並べ/棋譜再生モードの変更
  • 棋譜並べ/棋譜再生モード時の盤面反転機能追加
  • 縦横座標数値表示追加
  • 持ち時間・秒読み時間のカウント表示機能追加

5.その他

 おそらくこのようなアプリケーションは、webソケットを使用したシステムとして開発するのがベストだと思います。理由は、非同期で送られてくるshogi-serverからのデータをブラウザが取り出すために短い間隔でポーリングせざるを得ず、サーバーに負荷がかかるからです。でもDEN将棋では、着席可能な席数を減らすことによりこの問題を解決しています。使い方ページで「利用者が大勢集まる事に耐えるようにシステム設計していません。席数を増やすご要望にはお応えできませんことをあらかじめ御了承願います。」としたのはこのためです。

 現状shogi-serverの標準コマンドのみを実装していますが、拡張コマンドに対応すれば、先手後手指定対局・対局中のチャット・コマ落ち対局といったことができそうです。今後モチベーションが上がれば対応するかも。。