
いよいよ大詰めです。このページがラストです。ここでは以下のことについて考察します。
まず、完成後のプログラムを示します。
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;
おつかれさまでした。以上でさめがめの完成です。