さめがめを作る(その 4)

いよいよ大詰めです。このページがラストです。ここでは以下のことについて考察します。

まず、完成後のプログラムを示します。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Grids, Unit2, Contnrs, Types;

const
  MAX_COL_COUNT = 10;
  MAX_ROW_COUNT = 7;
  MAX_CELL_COUNT = MAX_COL_COUNT*MAX_ROW_COUNT;

  DOES_NOT_EXIST_CELL = -1;

type
  TForm1 = class(TForm)
    StringGrid1: TStringGrid;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure StringGrid1Click(Sender: TObject);
  private
    { Private 宣言 }
    FCells: array[1..MAX_CELL_COUNT] of TCell;
    FCurrentColCount: Integer;
    function GetEastIndex(x: Integer): Integer;
    function GetWestIndex(x: Integer): Integer;
    function GetNorthIndex(x: Integer): Integer;
    function GetSouthIndex(x: Integer): Integer;
    procedure InitializeCell;
    procedure SearchSameValue(CellIndex, FindValue: Integer);
    procedure SearchCell;
    procedure CompactCell;
    procedure DisplayCell;
    procedure DeleteCell;
    procedure CompactRow;
    procedure CompactCol;
    function ExistCell(Idx: Integer): Boolean;
    function  IsGameOver: Boolean;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}


{ TForm1 }

function TForm1.GetEastIndex(x: Integer): Integer;
begin
  if (x mod MAX_COL_COUNT) = 0 then
    Result := DOES_NOT_EXIST_CELL
  else
    Result := x+1;
end;

function TForm1.GetWestIndex(x: Integer): Integer;
begin
  if (x mod MAX_COL_COUNT) = 1 then
    Result := DOES_NOT_EXIST_CELL
  else
    Result := x-1;
end;

function TForm1.GetNorthIndex(x: Integer): Integer;
begin
  if (x-MAX_COL_COUNT) < 1 then
    Result := DOES_NOT_EXIST_CELL
  else
    Result := x-MAX_COL_COUNT;
end;

function TForm1.GetSouthIndex(x: Integer): Integer;
begin
  if (x+MAX_COL_COUNT) > MAX_CELL_COUNT then
    Result := DOES_NOT_EXIST_CELL
  else
    Result := x+MAX_COL_COUNT;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  Randomize;
  FCurrentColCount := MAX_COL_COUNT;

  for i := Low(FCells) to High(FCells) do
    FCells[i] := TCell.Create(Random(3));

  DisplayCell;
  Button1.Caption := 'ReStart';
end;

procedure TForm1.InitializeCell;
var
  i: Integer;
begin
  for i := Low(FCells) to High(FCells) do
    FCells[i].Num := Random(3);
  FCurrentColCount := MAX_COL_COUNT;
end;

procedure TForm1.FormDestroy(Sender: TObject);
var
  i: Integer;
begin
  for i := Low(FCells) to High(FCells) do
    FCells[i].Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  StringGrid1.Enabled := true;
  InitializeCell;
  DisplayCell;
end;

procedure TForm1.SearchSameValue(CellIndex, FindValue: Integer);

  function IsSameValue(Idx: Integer): Boolean;
  begin
    Result := (Idx <> DOES_NOT_EXIST_CELL) and (FCells[Idx].Num = FindValue);
  end;

  function NotYetKnowingCell(Idx: Integer): Boolean;
  begin
    Result := false;
    if not FCells[Idx].DeleteMe then begin
      Result := true;
      FCells[Idx].DeleteMe := true;
    end;
  end;

var
  Idx: Integer;
begin
  // 隣のセルが自分のセルの値と同じかどうかをしらべる。
  
  Idx := GetWestIndex(CellIndex);
  if IsSameValue(Idx) and NotYetKnowingCell(Idx) then
    SearchSameValue(Idx, FindValue);

  Idx := GetNorthIndex(CellIndex);
  if IsSameValue(Idx) and NotYetKnowingCell(Idx) then
    SearchSameValue(Idx, FindValue);

  Idx := GetEastIndex(CellIndex);
  if IsSameValue(Idx) and NotYetKnowingCell(Idx) then
    SearchSameValue(Idx, FindValue);

  Idx := GetSouthIndex(CellIndex);
  if IsSameValue(Idx) and NotYetKnowingCell(Idx) then
    SearchSameValue(Idx, FindValue);
end;

procedure TForm1.SearchCell;

  function GetCellIndex(Row, Col: Integer): Integer;
  begin
    Result := (Col+1) + MAX_COL_COUNT*Row;
  end;

var
  Idx: Integer;
begin
  Idx := GetCellIndex(StringGrid1.Row, StringGrid1.Col);
  SearchSameValue(Idx, FCells[Idx].Num);
end;

procedure TForm1.StringGrid1Click(Sender: TObject);
begin
  SearchCell;
  DeleteCell;
  CompactCell;
  DisplayCell;

  if IsGameOver then begin
    ShowMessage('Game Over');
    StringGrid1.Enabled := false;
  end;  
end;

procedure TForm1.DisplayCell;
var
  i, j, Idx: Integer;
  s: string;
begin
  for i := 1 to MAX_COL_COUNT do begin
    for j := 1 to MAX_ROW_COUNT do begin
      Idx := i + MAX_COL_COUNT*(j-1);
      if ExistCell(Idx) then
        s := IntToStr(FCells[Idx].Num)
      else
        s := '';
      StringGrid1.Cells[i-1, j-1] := s;
    end;
  end;
end;

procedure TForm1.CompactRow;
var
  i, j, Num: Integer;
  Stack: TStack;
  Idx: Integer;
begin
  Stack := TStack.Create;
  try
    for i := 1 to MAX_COL_COUNT do begin

      // 縦の列に対して、残っているセルを Stack に集める
      for j := 1 to MAX_ROW_COUNT do begin
        Idx := i + MAX_COL_COUNT*(j-1);
        if ExistCell(Idx) then Stack.Push(FCells[Idx]);
      end;

      // セルをつめる
      for j := MAX_ROW_COUNT downto 1 do begin
        if Stack.Count > 0 then
          Num := TCell(Stack.Pop).Num
        else
          Num := DOES_NOT_EXIST_CELL;
        Fcells[i+MAX_COL_COUNT*(j-1)].Num := Num;
      end;
    end;
  finally
    Stack.Free;
  end;
end;

procedure TForm1.DeleteCell;
var
  i: Integer;
begin
  for i := 1 to MAX_CELL_COUNT do
    if FCells[i].DeleteMe then begin
      FCells[i].Num := DOES_NOT_EXIST_CELL;
      FCells[i].DeleteMe := false;
    end;
end;

procedure TForm1.CompactCol;

  procedure DoCompactCol(EmptyList: TIntList);
  var
    i, j, n, LineCount: Integer;
  begin
    LineCount := 1;
    for i := 1 to FCurrentColCount do begin
      if EmptyList.IsInclude(i) then begin
        for j := 1 to MAX_ROW_COUNT do begin
          n := MAX_COL_COUNT*(j-1);
          FCells[LineCount+n].Num := FCells[i+n].Num;
        end;
        Inc(LineCount);
      end;
    end;

    for i := LineCount to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do
        FCells[i+MAX_COL_COUNT*(j-1)].Num := DOES_NOT_EXIST_CELL;

    FCurrentColCount := LineCount-1; // 列をつめたので、Col が短くなります。
  end;

  procedure CollectAliveCol(const IntList: TIntList);
  var
    i, j: Integer;
  begin
    for i := 1 to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do
        if ExistCell(i+MAX_COL_COUNT*(j-1)) then begin
          IntList.Add(i);
          break;
        end;
  end;

var
  ColList: TIntList;
begin
  ColList := TIntList.Create;
  try
    CollectAliveCol(ColList);
    if FCurrentColCount <> ColList.Count then
      DoCompactCol(ColList); // 実際につめる
  finally
    ColList.Free;
  end;
end;

function TForm1.IsGameOver: Boolean;

  function ExistSameValue: Boolean;
  var
    i: Integer;
  begin
    Result := false;
    for i := 1 to MAX_CELL_COUNT do
      if FCells[i].DeleteMe then begin
        Result := true;
        exit;
      end;
  end;

var
  i, j, Idx: Integer;
begin
  Result := true;
  try
    for i := 1 to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do begin
        Idx := i + MAX_COL_COUNT*(j-1);
        if ExistCell(Idx) then begin
          SearchSameValue(Idx, FCells[Idx].Num);
          if ExistSameValue then begin
            Result := false;
            exit;
          end;
        end;
      end;
  finally
    for i := 1 to MAX_CELL_COUNT do
      FCells[i].DeleteMe := false;
  end;
end;

procedure TForm1.CompactCell;
begin
  CompactRow;
  CompactCol;
end;

function TForm1.ExistCell(Idx: Integer): Boolean;
begin
  Result := FCells[Idx].Num <> DOES_NOT_EXIST_CELL;
end;

end.

Unit2

unit Unit2;

interface

type
  TCell = class
  private
    FNum: Integer;
    FDeleteMe: Boolean;
  public
    constructor Create(Num: Integer);
    property Num: Integer read FNum write FNum;
    property DeleteMe: Boolean read FDeleteMe write FDeleteMe;
  end;


  TIntList = class
  private
    FIntAry: array of Integer;
    FCount: Integer;
    FCapacity: Integer;
    procedure Grow;
  public
    constructor Create;
    procedure Add(Num: Integer);
    function IsInclude(Num: Integer): Boolean;
    property Count: Integer read FCount;
  end;

implementation

{ TCell }

constructor TCell.Create(Num: Integer);
begin
  Fnum := Num;
end;

{ TIntList }

procedure TIntList.Add(Num: Integer);
begin
  Inc(FCount);
  if FCount >= FCapacity then Grow;
  FIntAry[FCount-1] := Num;
end;

constructor TIntList.Create;
begin
  FCapacity := 5;
  FCount    := 0;

  SetLength(FIntAry, FCapacity);
end;

procedure TIntList.Grow;
begin
  Inc(FCapacity, FCapacity*2);
  SetLength(FIntAry, FCapacity);
end;

function TIntList.IsInclude(Num: Integer): Boolean;
var
  i: Integer;
begin
  Result := false;
  for i := 0 to FCount-1 do
    if FIntAry[i] = Num then begin
      Result := true;
      break;
    end;
end;

end.

少し長いですが、半分以上が今まで作成したコードです。新しく作成した関数は主に、TStringGrid をクリックした時に呼び出されます。

procedure TForm1.StringGrid1Click(Sender: TObject);
begin
  SearchCell;
  DeleteCell;
  CompactCell;
  DisplayCell;

  if IsGameOver then begin
    ShowMessage('Game Over');
    StringGrid1.Enabled := false;
  end;  
end;

TStringGrid がクリックされた時、次の処理を行います。

SearchCell は前回と全く同様のものです。DeleteCell は SearchCell 関数を呼び出した後に、DeleteMe プロパティが true になっているセルを消去します(実際には, 消去されたセルであるという目印の為に DOES_NOT_EXIST_CELL をセットします)

セル消去後、CompactCell 関数を呼び出して残されたセルをつめるわけですが、このとき、

という 2 つの作業があります。まず最初に縦に積めてから、その後で横につめるというようにように、それぞれ作業を分割して考えます。

CompactCell 関数は、縦方向にセルをつめるために CompactRow 関数を呼び出し、横方向のセルをつめるのに CompactCol 関数を呼び出します。

縦一列のセルをつめる様子を示したのが下図です。

消去されたセルを、その上に乗っているセルが押しつぶすような感じですね。CompactRow 関数の中身を見ていきましょう。

procedure TForm1.CompactRow;
var
  i, j, Num: Integer;
  Stack: TStack;
  Idx: Integer;
begin
  Stack := TStack.Create;
  try
    for i := 1 to MAX_COL_COUNT do begin

      // 縦の列に対して、残っているセルを Stack に集める
      for j := 1 to MAX_ROW_COUNT do begin
        Idx := i + MAX_COL_COUNT*(j-1);
        if ExistCell(Idx) then Stack.Push(FCells[Idx]);
      end;

      // セルをつめる
      for j := MAX_ROW_COUNT downto 1 do begin
        if Stack.Count > 0 then
          Num := TCell(Stack.Pop).Num
        else
          Num := DOES_NOT_EXIST_CELL;
        Fcells[i+MAX_COL_COUNT*(j-1)].Num := Num;
      end;
    end;
  finally
    Stack.Free;
  end;
end;

それぞれの(縦の)列の中にある(消去されていない)セルを、一旦 Stack の中に集めます。集め終わったら、後は順番に Stack から値をポップしながら、セルをつめていきます(※注意:Stack にプッシュしているのは、そのインデックスに対応した TCell のインスタンスの参照です。そして、ここでの「つめる作業」という意味は、実際には Stack からポップされた、TCell のインスタンスが保持している Num プロパティを、セルをつめるべき箇所に位置する TCell のインスタンスの Num プロパティにコピーするということ意味します)。

次に CompactCol で空行になっている列(縦方向のライン)があるかを調べ、存在すれば、その列をつめます。

基本的には、縦につめるときと同様です(上の図を時計回りに、ごろんと転がしたら、縦の場合といっしょになりますよね。まるっきり同じというわけではありませんが)。

procedure TForm1.CompactCol;

  procedure DoCompactCol(EmptyList: TIntList);
  var
    i, j, n, LineCount: Integer;
  begin
    LineCount := 1;
    for i := 1 to FCurrentColCount do begin
      if EmptyList.IsInclude(i) then begin
        for j := 1 to MAX_ROW_COUNT do begin
          n := MAX_COL_COUNT*(j-1);
          FCells[LineCount+n].Num := FCells[i+n].Num;
        end;
        Inc(LineCount);
      end;
    end;

    for i := LineCount to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do
        FCells[i+MAX_COL_COUNT*(j-1)].Num := DOES_NOT_EXIST_CELL;

    FCurrentColCount := LineCount-1; // 列をつめたので、Col が短くなります。
  end;

  procedure CollectAliveCol(const IntList: TIntList);
  var
    i, j: Integer;
  begin
    for i := 1 to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do
        if ExistCell(i+MAX_COL_COUNT*(j-1)) then begin
          IntList.Add(i);
          break;
        end;
  end;

var
  ColList: TIntList;
begin
  ColList := TIntList.Create;
  try
    CollectAliveCol(ColList);
    if FCurrentColCount <> ColList.Count then
      DoCompactCol(ColList); // 実際につめる
  finally
    ColList.Free;
  end;
end;

FCurrentColLine がセルがつめられた後の ColCount を保持します。TIntList クラスは、空でない縦の列を保持させるのに作成したクラスです。

TIntList クラスは Add メソッドと IsInclude メソッドを持ちます。Add メソッドで整数を追加し、IsInclude メソッドに渡された整数が、それまでにAdd で追加された整数かどうかを Boolean で返します。ここでの、このクラスの役割は、空の列でない列番号を保持させるのに使用しています。セル消去後に、生きているセルを縦に跨いだ空列があるなら、DoCompactCol を呼び出して、実際にセルをつめます。

いよいよ、最後の作業です。最後はゲーム終了の判定を行う IsGameOver 関数です。この関数は、もう消去できるセルがないかどうかの判断を、残っているセルに対して一つずつ愚直にSearchSameValue 関数を呼び出して調べ上げています。もし一つでも消去できる関数が見つかったなら、この関数は false を返します。true が返されるなら、そこでゲームを終了させます。

function TForm1.IsGameOver: Boolean;

  function ExistSameValue: Boolean;
  var
    i: Integer;
  begin
    Result := false;
    for i := 1 to MAX_CELL_COUNT do
      if FCells[i].DeleteMe then begin
        Result := true;
        exit;
      end;
  end;

var
  i, j, Idx: Integer;
begin
  Result := true;
  try
    for i := 1 to FCurrentColCount do
      for j := 1 to MAX_ROW_COUNT do begin
        Idx := i + MAX_COL_COUNT*(j-1);
        if ExistCell(Idx) then begin
          SearchSameValue(Idx, FCells[Idx].Num);
          if ExistSameValue then begin
            Result := false;
            exit;
          end;
        end;
      end;
  finally
    for i := 1 to MAX_CELL_COUNT do
      FCells[i].DeleteMe := false;
  end;
end;

おつかれさまでした。以上でさめがめの完成です。


up next
Last update: 2004/5/1