【DELPHI STARTER チュートリアルシリーズ】 シーズン2 第8回 ‟作ってみよう„ Delphiの部[JAPAN]

Posted by on in Programming


Delphi Starter Edition チュートリアルシリーズ 

シーズン2 第8回 「作ってみよう」Delphiの部

 


2017年1月23日より 「Delphi / C++Builder Starter チュートリアルシリーズ」 シーズン2、全9回、3月27日まで、毎週月曜日、Delphiパートが 17時00分~17時20分、 リアルタイム放送スペシャルコンテンツが5分~10分、C++Builderパートが 17時30分~17時50分の時間割でお送りしています。

無料でダウンロード & 利用できる開発環境のDelphi / C++Builder Starter エディションを使用して、プログラミング言語のDelphi (Object Pascal ), C++の基礎を学ぶオンラインセッションです。


[第8回 「作ってみよう」Delphiの部]

アジェンダ

  • ねらい
    • 第1~7回までの機能を使った Delphi(FireMonkey) プログラム
  • 実施内容
    • 三目並べゲーム

今回の第8回で紹介しているコードを実際にみれるサンプルをGithubに上げております。
以下のリンクからダウンロードしてご確認ください。

https://github.com/mojeld/embarcadero_noughts_and_crosses/


[ルール]

  • 9個(3x3)のボタン順に〇と×先に3つ並べた方が勝ち。
  • コンピュータと1対1の対戦をします。
  • 先攻・後攻が選べます。

[画面デザイン]


 

[三目並べ 処理] 

  • ゲームボタンのクリア
  • ボタンクリック”〇”を入れる
  • 三目並びリストからCOMが候補を選んで”×”を入れる
  • 三目が並んだかを判定
    • 三目パターン配列を作る
    • 勝敗判定とコンピュータ側の思考

 

 [三目並べ 処理クラスを作る]

////
  TMarupeke = class(TObject)
    constructor Create(const tim_: TTimer);
    destructor Destroy; override;
  private
    Fstart_flg:     Boolean;
    FButtons:   TList<TSpeedButton>;  //1~9までのボタン
    FMainTimer: TTimer;               //メイン側のタイマー
    function getButtons: TList<TSpeedButton>;
  public
    ///<summary>ボタンを呼出すプロパティ</summary>
    property  Buttons: TList<TSpeedButton>  read getButtons;
    procedure game_clear(start, attacked: Boolean; event_: TNotifyEvent);
    class procedure line_list(proc: TProc<TLines>);
    function triumph: Integer;
    function triumph_row(r: TLines): Integer;
    property  start_flg: Boolean  read Fstart_flg write Fstart_flg;
  end;

TList<>がジェネリッス型のクラスです。 http://docwiki.embarcadero.com/Libraries/Berlin/ja/System.Generics.Collections.TList

  • ジェネリックスは、アルゴリズム(手続き、関数など)やデータ構造(クラス、インターフェイス、レコードなど)を、そこで使用される 1 つ以上の特定の型から切り離せるようにする一連の抽象化ツールです。
  • 以前実施した DELPHI BOOT CAMP / DELPHIでビジュアル開発に挑戦しよう ◆ DAY3 にて ご紹介しています。

 

[クラス定義を別のユニットに分離]

  • ユニットの追加
    • メニューから [新規作成 | ユニット - Delphi] 
  • なぜ別クラスにするのか
    • 1つ型を作っていれば他で使える
  • なぜ別ユニットファイルに分けたのか
    • 別プロジェクトを作成した場合ファイルが共有できる

[三目並べ 処理クラスを作る]

  • メインフォームからTMarupekeクラスのインスタンスを作る
  • ////
    uses
      System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
      FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Layouts,
      FMX.Controls.Presentation, FMX.StdCtrls, FMX.ScrollBox, FMX.Memo, System.DateUtils,
      FMX.Objects, System.Generics.Collections, Unit2;
    

    usesにUnit2を追加します

  • ////
    type
      TForm1 = class(TForm)
        procedure SpeedButton12Click(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure Timer1Timer(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        Marupeke1:  TMarupeke;
      public
      end;
    

    TForm1のプライベート変数としてTMarupeke を追加

    (このフォームだけで使用するものなので privateのところに記述しています)

 


[OnCreateとOnDestroyにTMarupeke.Createの記述と、DisposeOfの記述を追加]

TMarupekeはMarupeke1と言う変数名で利用します。

そのための記述です

////
procedure TForm1.FormCreate(Sender: TObject);//フォーム作成時
var
  i:  Integer;
begin
  Marupeke1 := TMarupeke.Create(Timer1);  //TMarupekeインスタンスを作る
  for i := 0 to 8 do                      //1~9のボタンをTMarupekeにセット
    Marupeke1.Buttons.Add(
      TSpeedButton(Self.FindComponent(Format('SpeedButton%d', [i+1]))) );
  Timer1.Enabled          := False;
  SpeedButton10.IsPressed := True;
  Randomize;
  Marupeke1.game_clear(False, SpeedButton11.IsPressed, SpeedButtonsClick);
end;

procedure TForm1.FormDestroy(Sender: TObject);//フォーム終了時
begin
  Marupeke1.DisposeOf;  //TMarupeke捨てる
end;

 


[初期設定 ゲームボタン表示など全て初期化(クリア)する]

TMarupekeクラス内にgame_clear()を作る

//
procedure TMarupeke.game_clear(start, attacked: Boolean; event_: TNotifyEvent);
var
  btn_: TSpeedButton;
begin
  for btn_ in FButtons do  //FindComponentで1~9までのSpeedButtonを探す
    with btn_ do
    begin
      IsPressed := False;  //プレス
      OnClick   := event_; //共通クリックイベントの設定
      Text      := '';
      Tag       := 0;
      if start then
        Enabled := True
      else
        Enabled := False;
    end;
  FMainTimer.Enabled  := attacked;  //後攻ならCOMのターン
end;

TMarupeke.game_clear()をコールする

  • フォーム作成時や、最初からゲームを開始する時にこのメソッドを呼びます
///
  Marupeke1.game_clear(False, SpeedButton11.IsPressed, SpeedButtonsClick);

[ボタンクリック”〇”を入れる]

1-9までのボタン共通イベントを作成する

それぞれのTSpeedButtonのOnClickイベントに共通の関数をセットする。

//
procedure TForm1.SpeedButtonsClick(Sender: TObject);
begin
  ///三目9マスのボタンのクリックイベント
  with (Sender as TSpeedButton) do
  begin
    if Tag = 0 then
    begin
      IsPressed := True;
      Tag       := player;  ///Unit2の定数の値
      Text      := '〇';    ///プレーヤは先攻/後攻関係なく〇が入る
      if Marupeke1.triumph > 1 then
      begin  ///条件が揃ったらWinを出してゲームクリア
        ShowMessage('Win');
        Marupeke1.game_clear(False, SpeedButton11.IsPressed, SpeedButtonsClick);
      end;
      Timer1.Enabled  := True;  ///タイマーを使いコンピュータへターンを渡す
    end;
  end;
end;

 


[三目パターン配列を作る]

1-9までのボタン三目並べれるパターン

TMarupekeのクラスメソッド(line_list)を追加します。

////
class procedure TMarupeke.line_list(proc: TProc<TLines>);
const
  triumph_naname: array[0..2, 0..2] of Integer = ((0,4,8),(2,4,6),(0,0,0));
var
  i: Integer;
  i1: Integer;
  i2: Integer;
  z:  TLines;
  zi: Integer;
begin
  SetLength(z, 3);
  for i := 0 to 2 do
    for i1 := 0 to 2 do
    begin
      for i2 := 0 to 2 do
      begin
        case i of
        0:z[i2] := i2 + (i1*3);
        1:z[i2] := (i2*3) + i1;
        2: begin
          z[i2] := triumph_naname[i1,i2];
        end;
        end;
      end;
      if not ((z[0] = 0) and (z[1] = 0) and (z[2] = 0)) then
        proc(z);
    end;
end;

このパターン配列の利用は下記項目です

line_listで無名メソッドを使っています。


[Delphi での無名メソッド]

無名メソッドとは、名前が付いていないプロシージャや関数のことです。

変数に代入したりメソッドへのパラメータとして使用できるエンティティとしてコード ブロックを扱います。さらに、無名メソッドは、自らが定義されるコンテキストで変数を参照したりそれらの変数に値をバインドすることもできます。無名メソッドは、簡単な構文で定義および使用することができます。

 

以前実施した DELPHI BOOT CAMP / DELPHIでビジュアル開発に挑戦しよう ◆ DAY3 にて ご紹介しています。

https://community.embarcadero.com/blogs/entry/delphi-boot-camp-day3-japan

[無名メソッドを なぜ使うのか]

このコーディング方法を初めから覚えておくと後で困らない

  • 新しい技術でも特殊なものでも無い為 最初から無名メソッドありきで思考する方が有利です
  • ある程度覚えてからだと、このコーディングスタイルに慣れるまで時間がかかります

無名メソッドは、JAVAやC#, Node.jsなどにも使われている為 他の言語からDelphi移植する場合、容易に理解できる思考が身につきます


 

[コンピュータ側(×側)で次の手を考える]

コンピューター側の処理は全てタイマーイベントを使っています。

////
procedure TForm1.Timer1Timer(Sender: TObject);
var
  random_:    Integer;
  sp_button_: TSpeedButton;
  next_:      Boolean;
  dtStart:    TDateTime;
  kouho:      Integer;
  bflg:       Boolean;
  counts_:    TCounts;
begin
  Timer1.Enabled  := False;
  dtStart := IncSecond(Now, 1); ///タイムアウト用1秒以上経過するとタイムアウト処理
  next_   := False;
  kouho := -1;
  if Marupeke1.start_flg then
  begin
    bflg  := False;
    ///無名メソッドを使い次の手を考えさせます
    TMarupeke.line_list(procedure (line_: TLines) var i: Integer; begin
      counts_ := TCounts.Create(0,0);
      {************************************************************************}
      {     TLines配列に〇と×が何個入っているか調べる
            このコードではプレーヤ側のリーチ目だけを見ています。(保守的)
            Question: 自分(コンピュータ)のリーチ目を調べて自分から勝つコードを
                      追記修正してみてください。                               }
      {************************************************************************}
      for i:=0 to Length(line_)-1 do
      begin
        sp_button_  := TSpeedButton(Self.FindComponent(Format('SpeedButton%d', [line_[i]+1])));
        if sp_button_.Tag = player then
          Inc(counts_.count1);
        if sp_button_.Tag = computer then
          Inc(counts_.count2);

        if (sp_button_.Tag = 0) and (not bflg) then
        begin
          kouho := line_[i];//次の手の候補を選ぶ
        end;
      end;
      if (counts_.count1 >= 2) and (counts_ < TCounts.Create(3,0)) then
      begin //TLines配列に〇が2つある場合で配列が埋まっていない場合
        counts_ := TCounts.Create(0,0);
        bflg    := True;
      end;
    end);

    while (not next_)  do
    begin
      if kouho >= 0 then  //次の手があればそれを利用し
        random_ := kouho
      else                //手が何も無い場合randomで選ぶ
        random_ := Random(9);
      Sleep(100);
      sp_button_  := TSpeedButton(Self.FindComponent(Format('SpeedButton%d', [random_+1])));
      if sp_button_.Tag = 0 then
      begin ///ここでコンピュータ側の手を入れます。
        sp_button_.Tag        := computer;  ///定数
        sp_button_.Text       := '×';      ///コンピュータ側は先攻/後攻全て×
        sp_button_.IsPressed  := True;
        next_ := True;
      end
      else
        kouho := -1;

      if (dtStart < Now) then
      begin //無限ループしないようにタイムアウトを作る
        next_ := True;
        ShowMessage('draw');
      end;
    end;

    {*********************************************************************}
    {     triumphは三目並べれば1以上を返しています。
          Question: function TMarupeke.triumph: Integer;を変更し
                    Win/Loseを作ってみてください。                        }
    {*********************************************************************}
    if Marupeke1.triumph > 1 then
    begin //triumphで勝敗がついている場合
      ShowMessage('Win');
      Marupeke1.game_clear(False, SpeedButton11.IsPressed, SpeedButtonsClick);
    end;
  end;
end;

ここまででコンピュータ側の"×"攻撃部分は完了です。

このタイマーイベント内でTCountsを使っています。

////
  TCounts = record
    count1:     Integer;
    count2:     Integer;
    constructor Create(c1, c2: Integer); overload;
    class operator LessThan(a: TCounts; b: TCounts): Boolean;
  end;

このTCountsはcount1をプレーヤー、count2をコンピュータの入力された回数です

他タイマーイベント内にtriumphメソッドが呼ばれています。

////
function TMarupeke.triumph: Integer;
var
  answer_:  Integer;
begin
  answer_  := 0;
  line_list(procedure (z: TLines) begin
    if (z[1] > 0) and (z[2] > 0) then
      answer_ := triumph_row(z);  //三目揃ったか?

    if answer_ >1 then  //どちらかが三目揃った場合
    begin
      Fstart_flg := False;
      FMainTimer.Enabled  := False;
    end
  end);
  if not Fstart_flg then
    Result  := 2
  else
    Result  := answer_;
end;

function TMarupeke.triumph_row(r: TLines): Integer;
var
  com_count: Integer;
  iTag, temp_tag:       Integer;
  answer_: Integer;
begin
  answer_   := 0;
  iTag      := -1;
  for com_count := 0 to Length(r)-1 do
  begin
    temp_tag  := FButtons[r[com_count]].Tag;
    if not(temp_tag = 0) then
      if (not(com_count=0) and (temp_tag = iTag)) then
        Inc(answer_)
      else
        iTag  := temp_tag;
  end;
  Result  := answer_;
end;

これは三目揃ったのかを判断するメソッドです。

TMarupeke.triumph内部でさらにfunction TMarupeke.triumph_row(r: TLines): Integer;をコールしています。

 


タイマーイベント中に"Question"と言うコメントがあります。

////
      {     TLines配列に〇と×が何個入っているか調べる
            このコードではプレーヤ側のリーチ目だけを見ています。(保守的)
            Question: 自分(コンピュータ)のリーチ目を調べて自分から勝つコードを
                      追記修正してみてください。                               }

      {************************************************************************}

    {     triumphは三目並べれば1以上を返しています。
          Question: function TMarupeke.triumph: Integer;を変更し
                    Win/Loseを作ってみてください。                        }

ぜひチャレンジしてみてください。

あと先行は"○"。後攻は"×"などの処理を入れてオリジナルのゲームを作っていくのも楽しいかと思います。

近いうちに上記2つのQuestionの答えをGithubで更新かける予定をしています。


 

 

<< シーズン2 第7回 「オブジェクト指向」その1

 

 

 

 



About
Gold User, No rank,
Delphi / C++Builder blogger

Comments

Check out more tips and tricks in this development video: