ジャンケン・ゲーム

ジャンケンをするオブジェクトを作成して、対戦させましょう。

オブジェクト指向プログラムでは、オブジェクトと言うのは実際の事物のシミュレーションだそうです。ジャンケンゲームの場合は、ジャンケンをする対戦者が二人と審判がいるものと想定します。

先ず、対戦者をオブジェクトで表現しましょう。対戦者は戦略はそれぞれ違うと言っても、ジャンケンをするという共通の特徴があるので、共通するものは上位クラスにまとめてしまいます。そうすると、対戦者全員についての変更が生じた場合上位クラスのコードを変更すれば、その変更は対戦者全員に反映されるので楽です。

ジャンケンの対戦者は、ジャンケンポンでジャンケンをしなくてはならないので、ジャンケンをするメソッド pon を作ることにします。また、対戦者の名前が必要になることが多いので、属性 NAME が必要です。また、自分の出した手と相手の出した手を記憶して利用する必要がありますから、属性 JIBUN には自分の手を記憶したリストのリファレンスを、また、属性 AITE には相手の手を記憶したリストのリファレンスを割り当てます。また、後々のデバックのために debug メソッドも用意します。これらのことを考えてクラス Janker.pm を次のように作成します。

package Janker;
sub new {
    my $param = shift;
    my $class = ref($param) || $param;
    my $self = {};
    bless( $self, $class );
    $self->{JIBUN} = [];
    $self->{AITE} = [];
    $self->{NAME} = 'foo';
    return $self;
}

sub name {
    my ($self, $name) = @_;
    if ( $name ) { $self->{NAME} = $name };
    return $self->{NAME};
}

sub pon {
    return 'goo';
}

sub record {
    my $self = shift;
    my ( $jibun, $aite ) = @_;
    my $jibun_list = $self->{JIBUN};
    my $aite_list = $self->{AITE};
    unshift( @$jibun_list, $jibun );
    unshift( @$aite_list, $aite );
}

sub debug {
    my $self = shift;
    my $jibun_list = $self->{JIBUN};
    my $aite_list = $self->{AITE};
    print "NAME: ", $self->{NAME}, "\n";
    print "JIBUN: @$jibun_list\n";
    print "AITE: @$aite_list\n";
}
1;

ref 関数は引数がオブジェクトへのリファレンスのばあいそのオブジェクトが属するクラス名を返し、引数がリファレンスでない場合は偽値を返します。また、new 関数は、$object = new Class と言う呼び出し方のときは、第1引数がクラス名になり、$object->new() という呼び出し方のときは、第1引数がオブジェクトへのリファレンスになります。したがって、$class = ref($param) || $param では $class にはどちらの呼び出し方でもクラス名が入ることになります。これば次のようにメインプログラムで、オブジェクトのクローニングをできるようにするための工夫です。

$object_1 = new Class;
$object_2 = $objict_1->new();

次にそれぞれの戦略を持った、対戦者のクラスを作成します。対戦者の名前と、ジャンケンをするメソッドの pon はそれぞれの対戦者でオーバーライドします。先ず、対戦者山田さんのクラスを作成します。

package Yamada;
use Janker;
@ISA = qw( Janker );

sub new {
    my($param) = shift;
    my($class) = ref($param) || $param;
    my($self) = $class->SUPER::new();
    bless( $self, $class );
    $self->{NAME} = 'Yamada';
    return $self;
}

sub pon {
    return 'pah';
}
1;

SUPER という見慣れない名前が出てきますが、これは、上位クラスを表しています。上位クラスの名前をハードコードしなくて済むのでコードの再利用が容易になります。

コードを見れば分かるように山田さんの戦略はパーしか出さないと言うちょっと情けないかな、という戦略です。

次に対戦者田中さんのクラスを作成します。

package Tanaka;
use Janker;
@ISA = qw( Janker );

sub new {
    my($param) = shift;
    my($class) = ref($param) || $param;
    my($self) = $class->SUPER::new();
    bless( $self, $class );
    $self->{NAME} = 'Tanaka';
    return $self;
}

@tactics = ('goo', 'pah', 'choki', 'choki', 'goo', 'goo' );

sub pon {
    return shift( @tactics );
}
1;

田中さんの戦略は少し高尚で、@taktics に登録された通りに手を出して行くと言うものです。

これで、対戦者はそろいましたが、次に、この対戦者たちを戦わせ判定をする審判のクラスが必要です。審判はジャンケンの対戦回数を聞く round というメソッド、一回毎の勝ち負けの判定をする _judge というメソッド、対戦を運営する match というメソッドを持っています。_judge がアンダースコアで始まっているのはクラスの内部でだけ呼び出されることを示唆しています。また、審判はどちらが何回勝ったのか記憶しておかなければならないので、FIRST、SECOND、DRAWなどの属性を持たせます。これらのことを考慮して作成したクラス Referee.pm は次のようになります。

package Referee;
sub new {
    my $self = {};
    bless( $self );
    $self->{ROUND} = 1;
    $self->{FIRST} = 0;
    $self->{SECOND} = 0;
    $self->{DRAW} = 0;
    return $self;
}

sub round {
    my ($self, $round) = @_;
    $self->{ROUND} = $round;
    return $round;
}

sub match {
    my ($self) = shift;
    my ($first, $second) = @_;
    my ($name1) = $first->name;
    my ($name2) = $second->name;
    my ($round) = $self->{ROUND};
    my ($i);
    for ($i = 0; $i < $round; $i++) {
        $result = _judge( $first, $second );
        print "round", $i+1, ") \t";
        print "$name1: $ken1\t$name2: $ken2\t$message\n";
        if ( $result eq 'first' ) { $self->{FIRST}++ }
        elsif ($result eq 'second' ) { $self->{SECOND}++ }
        else { $self->{DRAW}++ };
    }
    my $one = $self->{FIRST};
    my $two = $self->{SECOND};
    my $aiko = $self->{DRAW};
    print "\n";
    print "$name1: $one\t$name2: $two\tAiko: $aiko\n";
    if ($one == $two) { print "Draw Game\n" }
    elsif ($one > $two) { print $name1, " is the WINNER!\n" }
    else { print $name2, " is the WINNER!\n" }
}

%ring = ( 'goo' => 'pah',
          'pah' => 'choki',
          'choki' => 'goo' );

sub _judge {
    my ( $first, $second ) = @_;
    my $name1 = $first->name;
    my $name2 = $second->name;
    $ken1 = $first->pon;
    $ken2 = $second->pon;
    $first->record($ken1, $ken2);
    $second->record($ken2, $ken1);
    my $win = $ring{$ken2};
    if ( $ken1 eq $ken2 ) {
        $message = "Aiko";
        return 'even' }
    elsif ( $ken1 eq $win ) {
        $message = "$name1 won!";
        return 'first' }
    else {
        $message = "$name2 won!";
        return 'second'}
}
1;

さあ、役者はそろったので、いよいよ対戦です。ジャンケン対戦のメインプログラムを作成します。手順は簡単です。Yamada, Tanaka, Referee の各モジュールからオブジェクトを作成します。次に $referee に 山田さんと田中さんの対戦を依頼します。対戦回数は5回とします。最期に 山田さんと、田中さんの debug メソッドを呼び出して反省会をします。そこで、次のスクリプトを match.pl と言う名で作成します。

#!/usr/bin/perl
use Referee;
use Yamada;
use Tanaka;

$yamada = new Yamada;
$tanaka = new Tanaka;
$referee = new Referee;

$referee->round(5);
$referee->match( $yamada, $tanaka );
print "\n";
$yamada->debug;
$tanaka->debug;

コンソールから perl match.pl と入力して対戦させてみましょう。

$ perl match.pl
round1)         Yamada: pah     Tanaka: goo     Yamada won!
round2)         Yamada: pah     Tanaka: pah     Aiko
round3)         Yamada: pah     Tanaka: choki   Tanaka won!
round4)         Yamada: pah     Tanaka: choki   Tanaka won!
round5)         Yamada: pah     Tanaka: goo     Yamada won!

Yamada: 2       Tanaka: 2       Aiko: 1
Draw Game

NAME: Yamada
JIBUN: pah pah pah pah pah
AITE: goo choki choki pah goo
NAME: Tanaka
JIBUN: goo choki choki pah goo
AITE: pah pah pah pah pah

山田さんもなかなか頑張ったようです。さいごの、デバッグで、対戦者の対戦結果の記憶は逆順になっているので先頭の要素が最新の対戦の記憶になります。

ジャンケンの戦略を変えるのは Janker クラスの pon メソッドをオーバーライドして Janker の派生クラスを作成するだけなので簡単です。色々と面白い戦略を考えてシミュレーションしておけば、実際のジャンケンも強くなるかもしれません。