対戦型五目並べ

アプレット〜ステータス・クラス

お題:State  オブジェクトの内部状態が変化したときに、オブジェクトが振る舞いを変えるようにする。クラス内では、振る舞いの変化を記述せず、状態を表すオブジェクトを導入することでこれを実現する。

さて、アプレットの最後のソースは、これもまた大物である。State パターンを使った Status 派生クラスである。Mediator パターンとのカラミで、外部のクラスとしては Mediator しか呼び出さず、結合性を低めているのが大きなポイントでもある。

State パターン自体は、Mediator のところでかなり説明している。それをちゃんと理解してから見た方がいいだろう。

もう一度 State パターンの説明を繰り返す。State パターンは、状態遷移機械の一つ一つの「状態」をオブジェクトにしたものである。だからこれは「状態」という「概念」が、オブジェクトになって実装される、と考えればよい。つまり、このプログラムだと Status を基底クラスとして、「自分の番の状態」である SelfStatus と、「相手の番の状態」である PartnerStatus の2つのクラスのオブジェクトが生成される(加えて終了状態の Status 基底クラス自身のオブジェクトも)。「現在の状態」を示す Status クラスの変数 now が Mediator の中にあり、通信によって手番の先後が決まるから、まず最初はこの変数 now に、先番なら SelfStatus オブジェクトが、後番なら PartnerStatus オブジェクトがセットされる。

そして、「自分の番」SelfStatus ならば盤面をクリックすることで、「自分の手」を相手に送ることができる。そうすると、「相手の番」PartnerStatus に遷移し(つまり、変数 now に PartnerStatus オブジェクトがセットされる)、今度は盤面をクリックしても「自分の手」が送られることはないが、「相手の手」を受け入れることができるようになる。ここで「相手の手」が通信によって届くと、今度は「自分の番」SelfStatus に遷移する(変数 now に SelfStatus オブジェクトがセットされる)。

こういう風に動くのだから、要するに実際の処理コードは、模式的に次のかたちになる。

   Status now = firstStatus();  /* 最初の状態 */
   while( ! (now instanceof EndStatus) ) {  /* 終了状態でない限り */
      now = now.action( getAction() );  /* 入力を受け入れて遷移 */
   }

このプログラムでは「終了状態」以外の「状態」は2つしかないが、沢山の状態のある状態機械を作っても勿論構わないのである。

基底クラスである Status クラスは、「終了状態」を示す具象クラスとしても実装されている。これは全体クラス数削減のためにしたことであり、気に入らなければ派生させてね。

で、具体的な「入力」にはどんな種類があるのだろう? これらを扱うハンドラが、実際には派生クラスで具体的に定義すべき抽象メソッドとなっていくので重要である。

public void doFirst();
これはアクションとは無関係に、最初に先手・後手が決まった時点で実行すべき準備である。具体的には表示をセットするくらいなことしかしてない。
public Status kosan();
「降参」ボタンを押すアクション。
public Status timeout();
「時間切れ」が生成した時のアクション。「相手の番」では起きるわけがない。
public Status mouse( Point at );
盤面をクリックした(「手」を特定した)時のアクション。「相手の番」で起きても無効に決まってる。
public Status hand( Point at );
さて、以下の4つが通信で送られてきたプロトコルに応じた処理をするわけである。これも一つ一つが「アクション」である。これは HAND プロトコルが送られてきた時のアクションである。本質的に「相手の番」でないと送られてこない。
public Status win( String s, Point at );
WIN プロトコルが送られてきた時のアクション
public Status lose( String s, Point at );
LOSE プロトコルが送られてきた時のアクション
public Status nogame( String s, Point at );
NOGAME プロトコルが送られてきた時のアクション
public Status fatal( String s );
これは内部エラーを仮想的に「内部エラー・プロトコル」が送られてきたかのように扱うので、スレッド内部でエラーが生じたアクションである。

GoApplet/Status.java

このクラスには3つの働きがある。

  1. 動的ローディングで派生クラスを生成するクラスメソッド create() を持つ。
  2. Status 派生クラスの基底クラスとして使われる。
  3. これ自身として、「終了状態」を示す。

まあ、「基底クラス=終了状態」なので、各ハンドラのデフォルトは当然 return this; で、状態遷移をしないものということになる。

package GoApplet;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.awt.event.*;
import GoRule.Rule;

/* このクラス及びこれから派生するクラスが、デザインパターンでいう
   「State」パターンを実現する。つまり、「状態」をオブジェクト化
   したものが、互いに「状態」を切り替えながら動作していくのであ
   る。基底クラスがこの Status クラスなのだが、同時に終了状態を
   も兼ねている。*/
public class Status {
   /* これらは派生クラスで共通のインスタンスを参照すべきものである */
   protected Rule rule;    /* 五目並べのルールを具体化 */
   protected Mediator md;  /* メディエータ */
   private Hashtable toGo; /* 遷移先が検索できる Hashtable */

   /* 動的ローディングをするので無引数コンストラクタ */
   public Status( ) { }

   /* 静的メソッドで動的ローディング風に派生クラスの
    インスタンスを返す。*/
   public static Status create( String cls ) throws Exception {
      Status ret;
      Class c = Class.forName( cls );
      return (Status)c.newInstance();
   }


   /* 必要なコンテキストをセットする
      特に重要なのは遷移先を格納した Hashtable である。
      要するに、すべての状態のインスタンスを生成して
      からでないと、遷移先テーブルを与えることができない
      わけである。*/
   public void init( Mediator m, Rule r, Hashtable h ) {
      /* 派生クラスで共有するインスタンスをセットする */
      md = m;
      rule = r; toGo = h;
   }

   /* State パターンで、遷移先を与える手段はいろいろあるが、
    まあ、Hashtable で文字列による検索が一番穏当だろう。
    もちろん引数で渡してもよいのだが、これは状態が多くなると
    結構混乱する。Hashtable なら大丈夫だな。ただし、
    3つの状態しかないから、Hashtable はちょっと不効率か。*/
   /* 文字列引数によって、遷移すべき状態を検索して返す。 */
   protected final Status getNext( String s ) {
      if( toGo == null ) {
         return this;
      }
      Status ret = (Status)toGo.get( s );
      if( ret == null ) {
         return this;
      } else {
         return ret;
      }
   }

   /* さて、以下が派生クラスで実装すべきメソッドである。 */
   /* Status 派生クラスのメソッドは、何かのアクションが
      生じた時に、状態に応じた処理をしてから、「次の状態」
      を戻り値として返すものである。*/

   /* このクラスは終了状態=もうどこにも遷移しない最後の
    状態を示すので、何もせず、当然自分自身を返している。*/

   /* 初期化を行うメソッド */
   public void doFirst() { }

   /* これらはクライアントのアクションに応じて、
    「状態」に応じた処理と遷移を担当する。*/
   public Status kosan() { return this; }
   public Status timeout() { return this; }
   public Status mouse( Point at ) { return this; }

   /* これらはサーバから通信されてきたプロトコルに
    よって、「状態」に即した処理と遷移を担当する。*/
   public Status hand( Point at ) { return this; }
   public Status win( String s, Point at ) { return this; }
   public Status lose( String s, Point at ) { return this; }
   public Status nogame( String s, Point at ) { return this; }

   /* これは内部エラー(ソケット切断の検知)のハンドラ。
      処理は共通 */
   public Status fatal( String s ) {
      md.showMessageA( "エラー!!", Color.red );
      md.showMessageB( s );
      md.appletEnd();
      return getNext( "selfS:end" );
   }
}

GoApplet/SelfStatus.java

さて、これは「自分の番」を示す「状態」である。「自分の番」の時は盤面をクリックして「手」を相手に送れたりするのだが、基本的に相手からの「手」は受け付けない。そういう風に実装されている。

また、このクラスだけ GoRule.Rule を使っている。これは以前にも勝負判定で述べたが、五目並べのルールをクラス化したものが GoRule パッケージである。しかし、クライアントでは勝負の判定はしないので、その基底クラスとして使う GoRule.Rule クラスをインスタンス化して、クリックされた石の位置が正当なものか(すでに石が置かれている場所でないか)だけを判定する。

package GoApplet;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.awt.event.*;
import GoRule.Rule;

/* State パターンの派生クラスの一つで、「自分の番」の状態を
   示すクラスである。五目並べは「自分の番」と「相手の番」が
   あり、それぞれで可能なアクションが異なる。このうち「自分
   の番」は、クリックで石を置いたりできるわけである。*/

/* また、Mediator パターンを使っているので、Status 派生クラス
   はほぼメディエーターだけを呼び出すことに注意されたい。*/
public class SelfStatus extends Status {
   /* これは初期化。先手番である。 */
   public void doFirst() {
      md.showMessageA( "あなたが先手です", md.getYourColor() );
      md.timerStart();      /* タイマーの開始 */
   }

   /* 以下、アプレットのアクションによって生成する入力の
      処理。*/

   /* 「降参」ボタンを押したアクション */
   public Status kosan() {
      md.timerEnd();      /* タイマーを止める */
      md.showMessageA( md.getPartnerHandle() + "の勝ちです" , Color.red );
      md.showMessageB( "降参!!          " );
      md.commReplyWin( "降参!!" ); /* サーバに送信する */
      md.appletEnd();      /* アプレットの終了 */
      return getNext( "selfS:end" );
   }

   /* 時間切れになる */
   public Status timeout() {
      md.showMessageA( md.getPartnerHandle() + "の勝ちです" , Color.red );
      md.showMessageB( "時間切れ           " );
      md.commReplyWin( "時間切れ" );
      md.appletEnd();
      return getNext( "selfS:end" );
   }

   /* 盤面をクリックしたら */
   public Status mouse( Point at ) {
      /* クリックした位置の正当性をチェックする */
      try {
         if( ! rule.canSetIshi( at ) ){
            /* すでに石がある場所の場合、クリックは
               無効。*/
            return this;
         }
      } catch( ArrayIndexOutOfBoundsException e ) {
         /* まあ、ありえんけど。 */
         return this;
      }
      md.timerEnd();
      /* 盤面に石を表示 */
      md.drawIshi( md.getYourColor(), at );
      /* 二重のチェックのため、置いた石は保存 */
      rule.setIshi( at, 1 );
      md.commReplyHand( at ); /* 手を送信 */
      /* 表示の変更 */
      md.showMessageA( md.getPartnerHandle() + "の番です", md.getPartnerColor() );
      md.showMessageB( "相手の手を待っています" );
      return getNext( "selfS:next" );
   }

   /* 以下、サーバとの通信で生成するアクション */
   /* 「自分の番」の時には本質的には起きない */
   /*  どうせ親クラスで定義しているままである。参考のため示す
       public Status hand( Point at ) { return this; }
       public Status win( String s, Point at ) { return this; }
       public Status lose( String s, Point at ) { return this; }
   */

   /* これだけ起きる可能性がある(全体タイムアウト)。 */
   public Status nogame( String s, Point at ) { 
      md.showMessageA( "引き分け", Color.red );
      md.showMessageB( s );
      md.appletEnd();
      return getNext( "selfS:end" );
   }
}

GoApplet/PartnerStatus.java

このクラスは「相手の番」の状態をオブジェクト化したものである。「相手の番」の時、自分の「手」を送信することはできないが、相手から送られてきた手を受け入れることができる。これが主目的である。

この時、プロトコルに応じて生成された CommItem 派生クラスのオブジェクトから、このハンドラを呼び出すに当たって、Visitor パターンが使われていることはすでに説明した。だから、この hand, win, lose, nogame のハンドラは基本的に Visitor パターンの visit() メソッドのようなものである。何種類もあるので、名前で区別するようにしているのである。

package GoApplet;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.awt.event.*;
import GoRule.Rule;

/* State パターンの派生クラスの一つで、「相手の番」の状態を
   示すクラスである。五目並べは「自分の番」と「相手の番」が
   あり、それぞれで可能なアクションが異なる。このうち「相手
   の番」は、クリックで石を置いたりできないが、サーバからの
   通信によっていろいろな処理をしなくてはならない。*/

/* また、Mediator パターンを使っているので、Status 派生クラス
   はほぼメディエーターだけを呼び出すことに注意されたい。*/
public class PartnerStatus extends Status {
   /* 最初の処理 */
   public void doFirst() { 
      md.showMessageA( md.getPartnerHandle() + "が先手です", md.getYourColor() );
   }

   /* 以下、アプレットのアクションによって生成する入力の
      処理。基本的に発生しないのでおざなりである*/

   /* 「降参」だけはできる */
   public Status kosan() { 
      md.timerEnd();
      md.showMessageA( md.getPartnerHandle() + "の勝ちです" , Color.red );
      md.showMessageB( "ゲーム放棄         " );
      md.commReplyWin( "降参!!" );
      md.appletEnd();
      return getNext( "partnerS:end" );
   }

   /* 時間切れは発生しないし、盤面をクリックしても無効である */
   /*  どうせ親クラスで定義しているままである。参考のため示す
       public Status timeout() { return this; }
       public Status mouse( Point at ) { return this; }
   */

   /* 以下、サーバとの通信で生成するアクション */
   /* PartnerStatus ではここが重要。サーバからの通信
    の種別によって、複雑な処理をする。*/

   /* HAND プロトコルが送られてきた */
   public Status hand( Point at ) { 
      /* 送られてきた手の正当性のチェック */
      /* ホントは異常だと対応のしようがないが... */
      try {
         if( ! rule.canSetIshi( at ) ){
            md.showMessageA( "エラー!!", Color.red );
            md.showMessageB( "HAND の二重置き" );
            md.appletEnd();
            return getNext( "partnerS:end" );
         }
      } catch( ArrayIndexOutOfBoundsException e ) {
         md.showMessageA( "エラー!!", Color.red );
         md.showMessageB( "HAND の範囲外" );
         md.appletEnd();
         return getNext( "partnerS:end" );
      }
      /* 正当性チェックのため手を保存 */
      rule.setIshi( at, 2 );

      /* 盤面に手を表示する */
      md.drawIshi( md.getPartnerColor(), at );
      md.timerStart(); /* タイマーの開始 */
      md.showMessageA( "あなたの番です", md.getYourColor() );
      md.showMessageB( "あと60秒          " );
      /* これは「自分の番」に遷移 */
      return getNext( "partnerS:next" ); 
   }

   /* WIN プロトコルが送られてきた */
   public Status win( String s, Point at ) { 
      if( at != null ) {
         /* 「手」がある場合もある */
         /* 正当性チェックでエラーしても対応しない */
         try {
            if( rule.canSetIshi( at ) ){
               rule.setIshi( at, 2 );
               md.drawIshi( md.getPartnerColor(), at );
            }
         } catch( ArrayIndexOutOfBoundsException e ) { }
      }
      md.showMessageA( "あなたの勝ちです", Color.red );
      md.showMessageB( s );
      md.appletEnd();
      return getNext( "partnerS:end" ); 
   }

   /* LOSE プロトコルが送られてきた */
   public Status lose( String s, Point at ) { 
      if( at != null ) {
         /* 「手」がある場合もある */
         /* 正当性チェックでエラーしても対応しない */
         try {
            if( rule.canSetIshi( at ) ){
               rule.setIshi( at, 2 );
               md.drawIshi( md.getPartnerColor(), at );
            }
         } catch( ArrayIndexOutOfBoundsException e ) { }
      }
      md.showMessageA( md.getPartnerHandle() + "の勝ちです", Color.red );
      md.showMessageB( s );
      md.appletEnd();
      return getNext( "partnerS:end" ); 
   }

   /* NOGAME プロトコルが送られてきた */   
   public Status nogame( String s, Point at ) {
      if( at != null ) {
         /* 「手」がある場合もある */
         /* 正当性チェックでエラーしても対応しない */
         try {
            if( rule.canSetIshi( at ) ){
               rule.setIshi( at, 2 );
               md.drawIshi( md.getPartnerColor(), at );
            }
         } catch( ArrayIndexOutOfBoundsException e ) { }
      }
      md.showMessageA( "引き分け", Color.red );
      md.showMessageB( s );
      md.appletEnd();
      return getNext( "partnerS:end" );
   }
}



copyright by K.Sugiura, 1996-2006