Главная » Delphi » Простое приложение Tic Tac Toe

0

Но достаточно теории! Настало время  применить полученные знания о COM+ на практике. Средства COM+ поставляются вместе  с простым приложением tic-tac- toe (крестики нолики), которое вдохновило автора реализовать эту классическую иг ру при помощи Delphi. Запустите мастер  Transactional Object Wizard и создайте новый объект под  названием GameServer. При  помощи редактора Type  Library  Editor  до бавьте  в интерфейс IGameServer, созданный для нового объекта по умолчанию, три метода:  NewGame(), ComputerMove(), и PlayerMove(). Добавьте также  два новых перечислимых  свойства: SkillLevels и  GameResults —  которые будут использо ваться  вышеуказанными методами. Все эти  элементы отображены в окне  редактора Type Library Editor, показанном на рис. 18.16.

Рис. 18.16. Сервер Tic-Tac-Toe в окне редактора Type Library EditorТри  новых  метода  данного интерфейса очень  просты и  служат  для  реализации компьютерной версии игры  в крестики нолики. Метод  NewGame служит  для инициа лизации новой игры  со стороны клиента. Метод ComputerMove анализирует возмож ные  ходы  и выполняет ход со стороны компьютера. Метод  PlayerMove позволяет сделать ход клиенту. Ранее уже упоминалось о  том,  что  подход  к разработке  компо нентов COM+ отличается от подхода  к разработке стандартных компонентов COM. Хорошей иллюстрацией этому является приложение tic-tac-toe.

Если бы это был заурядный компонент COM, то для учета состояния игры в методе

NewGame() понадобилось бы просто инициализировать соответствующую  структуру данных.  Такой  структуре  могло  бы соответствовать поле  экземпляра объекта, к кото рому могли бы иметь доступ другие методы.

Почему  же такой  подход  нельзя  использовать при  разработке компонента COM+? Все дело  в учете  состояния. Как уже упоминалось ранее, для использования всех  воз можностей COM+ объект не должен  хранить информацию о своем состоянии. Но структура компонента ориентирована на обработку данных экземпляра между вызовами методов,  что   соответствует  учету  состояния.  Поэтому   при   разработке  компонента COM+ лучше всего из метода  NewGame() возвращать идентификатор игры  и использо вать его для обработки структур данных  при помощи специальных средств  распределе ния  ресурсов. Такие  средства должны  работать вне контекста экземпляра объекта, по тому что средства COM+ могут активизировать и дезактивировать экземпляры объекта при  каждом  вызове метода.  Все остальные методы  компонента могут принимать иден тификатор игры  в качестве параметра и извлекать данные из совместно используемого ресурса.  При  таком  подходе  состояние не учитывается, так как объект может  не быть активным между вызовами метода,  а все остальные методы  принимают необходимые данные через параметры и средства распределения ресурсов.

Средства доступа к совместно используемым данным  в COM+ называются распреде лителями ресурсов (resource dispenser). В частности, распределитель ресурсов Shared Property Manager используется для  обработки общих  данных  компонента. Доступ  к этому  распределителю ресурсов организован через интерфейс  ISharedProperty- GroupManager. Распределитель ресурсов Shared Property Manager находится на верхнем уровне  иерархии системы запоминающих устройств и поддерживает любое количество групп совместно используемых свойств  через интерфейс ISharedProp- ertyGroup. В свою очередь, каждая такая группа может  содержать любое количество совместно используемых свойств, доступ  к которым организован через интерфейс ISharedProperty. Совместно используемые свойства удобны  тем,  что  они  исполь зуются  в спецификации COM+ вне  контекста экземпляров любых  объектов, а управ ление  доступом к ним осуществляется через блокировки и семафоры распределителя ресурсов Shared Property Manager.

С  учетом  всего  вышесказанного  реализация  метода  NewGame() будет  иметь  сле

дующий вид:

procedure TGameServer.NewGame(out GameID: Integer);

var

SPG: ISharedPropertyGroup;

SProp: ISharedProperty;

Exists: WordBool;

GameData: OleVariant;

begin

// При проверке безопасности использовать роль пользователя

CheckCallerSecurity;

// Получить указатель на группу совместно используемых свойств

SPG := GetSharedPropertyGroup;

// Создать или получить совместно используемое

// свойство NextGameIDSProp := SPG.CreateProperty(‘NextGameID’, Exists);

if Exists then GameID := SProp.Value

else GameID := 0;

// Увеличить и сохранить значение свойства NextGameID

SProp.Value := GameID + 1;

// Создать массив данных игры

GameData := VarArrayCreate([1, 3, 1, 3], varByte);

SProp := SPG.CreateProperty(Format(GameDataStr,

[GameID]), Exists);

SProp.Value := GameData; SetComplete;

end;

В данном  методе  сначала  проверяется роль  пользователя, а затем  для получения идентификатора игры  применяется  совместно используемое свойство. После  этого создается вариантный массив  для хранения данных  игры, а затем  эти  данные  сохра няются в совместно используемом свойстве. В завершение вызывается метод SetCom- plete(), извещающий средства COM+ о возможности деактивации данного экземп ляра.

Исходя  из этого, можно  сформулировать главное правило разработки компонен тов  COM+: вызывайте метод  SetComplete() или  SetAbort() настолько часто,  на сколько  это  возможно. В идеале  один  из таких  методов должен  вызываться во всех методах  компонента, так как в этом случае средства COM+ смогут восстанавливать ре сурсы,  потребляемые  экземпляром компонента,  после  выхода   из  каждого   метода. Другим  словами, в процессе активизации и деактивации объекта не должно  потреб ляться много ресурсов.

Реализация  метода  CheckCallerSecurity() иллюстрирует  преимущества  роле

вой системы безопасности COM+:

procedure TGameServer.CheckCallerSecurity;

begin

// Игра только для членов роли "TTT".

if IsSecurityEnabled and not IsCallerInRole(‘TTT’) then

raise Exception.Create(‘Only TTT role can play tic-tac-toe’);

end;

При  изучении данного фрагмента возникает вопрос: как  установить роль  TTT и определить ее членов?  Роли можно определять и программно, однако лучше для этого использовать средство проводника сервера транзакций. После  установки компонента можно  установить роли  при  помощи узла Roles, расположенного под узлом каждого пакета  в окне  Transaction Server Explorer. Важно  отметить, что ролевая система  безо пасности поддерживается только  компонентами, которые используются  в системах Windows  NT.  Для  компонентов, которые используются в системах Windows  9x/Me, метод IsCallerInRole() всегда возвращает значение True.

Ниже  представлена реализация методов ComputerMove() и PlayerMove():

procedure TGameServer.ComputerMove(GameID: Integer; SkillLevel: SkillLevels; out X, Y: Integer;

out GameRez: GameResults);

varExists: WordBool; PropVal: OleVariant; GameData: PGameData; SProp: ISharedProperty;

begin

// Получить совместно используемое свойство,

// содержащее данные игры

SProp := GetSharedPropertyGroup.CreateProperty(Format

?(GameDataStr, [GameID]), Exists);

// Получить массив данных игры и блокировки для него,

// обеспечивающие более эффективный доступ.

PropVal := SProp.Value;

GameData := PGameData(VarArrayLock(PropVal));

try

// Если игра не окончена, предоставить право хода компьютеру

GameRez := CalcGameStatus(GameData);

if GameRez = grInProgress then begin

CalcComputerMove(GameData, SkillLevel, X, Y);

// Временно сохранить новый массив данных игры

SProp.Value := PropVal;

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);

end;

finally

VarArrayUnlock(PropVal);

end;

SetComplete;

end;

procedure TGameServer.PlayerMove(GameID, X, Y: Integer;

out GameRez: GameResults);

var

Exists: WordBool;

PropVal: OleVariant;

GameData: PGameData;

SProp: ISharedProperty;

begin

// Получить совместно используемое свойство,

// содержащее данные игры

SProp := GetSharedPropertyGroup.CreateProperty(

?Format(GameDataStr, [GameID]), Exists);

// Получить массив данных игры и блокировки для него,

// обеспечивающие более эффективный доступ.

PropVal := SProp.Value;

GameData := PGameData(VarArrayLock(PropVal));

try

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);

if GameRez = grInProgress then begin

// Если клетка не пуста, то передать исключение

if GameData[X, Y] <> EmptySpot then

raise Exception.Create(‘Spot is occupied!’);

// Сделать ходGameData[X, Y] := PlayerSpot;

// Временно сохранить новый массив данных игры

SProp.Value := PropVal;

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);

end;

finally

VarArrayUnlock(PropVal);

end;

SetComplete;

end;

Эти  методы  похожи тем,  что  оба  они  принимают данные игры  из совместно ис пользуемого свойства через параметр GameID, обрабатывают эти данные  для отобра жения текущего  хода, сохраняют обновленные данные  и проверяют окончание игры. Метод  ComputerMove() также  вызывает метод  CalcComputerMove(), анализирую щий состояние данных  игры, и выполняет следующий  ход. Весь исходный код этого компонента COM+, содержащийся в модуле ServMain, представлен в листинге 18.6.

Листинг 18.6. Модуль ServMain.pas, содержащий класс TGameServer

unit ServMain;

interface uses

ActiveX, MtsObj, Mtx, ComObj, TTTServer_TLB, StdVcl;

type

PGameData = ^TGameData;

TGameData = array[1..3, 1..3] of Byte;

TGameServer = class(TMtsAutoObject, IGameServer)

private

procedure CalcComputerMove(GameData: PGameData;

Skill: SkillLevels;

var X, Y: Integer);

function CalcGameStatus(GameData: PGameData): GameResults;

function GetSharedPropertyGroup: ISharedPropertyGroup;

procedure CheckCallerSecurity;

protected

procedure NewGame(out GameID: Integer); safecall;

procedure ComputerMove(GameID: Integer;

SkillLevel: SkillLevels;

out X, Y: Integer;

out GameRez: GameResults); safecall;

procedure PlayerMove(GameID, X, Y: Integer;

out GameRez: GameResults); safecall;

end;

implementationuses ComServ, Windows, SysUtils, Variants;

const

GameDataStr = ‘TTTGameData%d';

EmptySpot = 0;

PlayerSpot = $1;

ComputerSpot = $2;

function TGameServer.GetSharedPropertyGroup: ISharedPropertyGroup;

var

SPGMgr: ISharedPropertyGroupManager;

LockMode, RelMode: Integer;

Exists: WordBool;

begin

if ObjectContext = nil then

raise Exception.Create(‘Failed to obtain object context’);

// Создать для объекта группу совместно используемых свойств

OleCheck(ObjectContext.CreateInstance(CLASS_SharedProp

?ertyGroupManager, ISharedPropertyGroupManager, SPGMgr)); LockMode := LockSetGet;

RelMode := Process;

Result := SPGMgr.CreatePropertyGroup(‘DelphiTTT’, LockMode,

RelMode, Exists);

if Result = nil then

raise Exception.Create(‘Failed to obtain property group’);

end;

procedure TGameServer.NewGame(out GameID: Integer);

var

SPG: ISharedPropertyGroup;

SProp: ISharedProperty;

Exists: WordBool;

GameData: OleVariant;

begin

// При проверке безопасности использовать роль пользователя

CheckCallerSecurity;

// Получить указатель на группу совместно используемых свойств

SPG := GetSharedPropertyGroup;

// Создать или получить совместно используемое

// свойство NextGameID

SProp := SPG.CreateProperty(‘NextGameID’, Exists);

if Exists then GameID := SProp.Value

else GameID := 0;

// Увеличить и сохранить значение свойства NextGameID

SProp.Value := GameID + 1;

// Создать массив данных игры

GameData := VarArrayCreate([1, 3, 1, 3], varByte);

SProp := SPG.CreateProperty(Format(GameDataStr, [GameID]),

Exists);

SProp.Value := GameData; SetComplete;

end;procedure TGameServer.ComputerMove(GameID: Integer; SkillLevel: SkillLevels; out X, Y: Integer;

out GameRez: GameResults);

var

Exists: WordBool;

PropVal: OleVariant;

GameData: PGameData;

SProp: ISharedProperty;

begin

// Получить совместно используемое свойство,

// содержащее данные игры

SProp := GetSharedPropertyGroup.CreateProperty(

?Format(GameDataStr, [GameID]), Exists);

// Получить массив данных игры и блокировки для него,

// обеспечивающие более эффективный доступ.

PropVal := SProp.Value;

GameData := PGameData(VarArrayLock(PropVal));

try

// Если игра не окончена, предоставить право хода компьютеру

GameRez := CalcGameStatus(GameData);

if GameRez = grInProgress then begin

CalcComputerMove(GameData, SkillLevel, X, Y);

// Временно сохранить новый массив данных игры

SProp.Value := PropVal;

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);

end;

finally

VarArrayUnlock(PropVal);

end;

SetComplete;

end;

procedure TGameServer.PlayerMove(GameID, X, Y: Integer;

out GameRez: GameResults);

var

Exists: WordBool;

PropVal: OleVariant;

GameData: PGameData;

SProp: ISharedProperty;

begin

// Получить совместно используемое свойство,

// содержащее данные игры

SProp := GetSharedPropertyGroup.CreateProperty(

?Format(GameDataStr, [GameID]), Exists);

// Получить массив данных игры и блокировки для него,

// обеспечивающие более эффективный доступ.

PropVal := SProp.Value;

GameData := PGameData(VarArrayLock(PropVal));

try

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);if GameRez = grInProgress then begin

// Если клетка не пуста, то передать исключение

if GameData[X, Y] <> EmptySpot then

raise Exception.Create(‘Spot is occupied!’);

// Сделать ход

GameData[X, Y] := PlayerSpot;

// Временно сохранить новый массив данных игры

SProp.Value := PropVal;

// Проверить окончание игры

GameRez := CalcGameStatus(GameData);

end;

finally

VarArrayUnlock(PropVal);

end;

SetComplete;

end;

function TGameServer.CalcGameStatus(GameData:

PGameData): GameResults;

var

I, J: Integer;

begin

// Проверить, нет ли победителя

if GameData[1, 1] <> EmptySpot then begin

// Проверить верхнюю строку, левый столбец и диагональ из

// верхнего левого в правый нижний угол

if ((GameData[1, 1] = GameData[1, 2]) and

(GameData[1, 1] = GameData[1, 3])) or

((GameData[1, 1] = GameData[2, 1]) and

(GameData[1, 1] = GameData[3, 1])) or

((GameData[1, 1] = GameData[2, 2]) and

(GameData[1, 1] = GameData[3, 3])) then

begin

Result := GameData[1, 1] + 1; // Результат – идентификатор

Exit;                                 // клетки + 1

end;

end;

if GameData[3, 3] <> EmptySpot then begin

// Проверить нижнюю строку и правый столбец

if ((GameData[3, 3] = GameData[3, 2]) and

(GameData[3, 3] = GameData[3, 1])) or

((GameData[3, 3] = GameData[2, 3]) and

(GameData[3, 3] = GameData[1, 3])) then

begin

Result := GameData[3, 3] + 1; // Результат – идентификатор

Exit;                                 // клетки + 1

end;

end;

if GameData[2, 2] <> EmptySpot then begin

// Проверить среднюю строку, средний столбец и диагональ из

// нижнего левого в верхний правый угол

if ((GameData[2, 2] = GameData[2, 1]) and

(GameData[2, 2] = GameData[2, 3])) or((GameData[2, 2] = GameData[1, 2]) and (GameData[2, 2] = GameData[3, 2])) or ((GameData[2, 2] = GameData[3, 1]) and

(GameData[2, 2] = GameData[1, 3])) then begin

Result := GameData[2, 2] + 1; // Результат – идентификатор

Exit;                                 // клетки + 1

end;

end;

// Наконец, определяем необходимость продолжения игры

for I := 1 to 3 do

for J := 1 to 3 do

if GameData[I, J] = 0 then begin

Result := grInProgress;

Exit;

end;

// Если игрок является членом роли TTT, то он

// автоматически побеждает.

if IsCallerInRole(‘TTT’) then begin

Result := grPlayerWin;

Exit;

end;

// В противном случае – ничья

Result := grTie;

end;

procedure TGameServer.CalcComputerMove(GameData: PGameData; Skill: SkillLevels; var X, Y: Integer);

type

// Используется для поиска возможных ходов по вертикали,

// горизонтали или диагонали

TCalcType = (ctRow, ctColumn, ctDiagonal);

// mtWin = один ход до победы, mtBlock = оппоненту остается один

// ход до победы, mtOne = я заполнил еще одну клетку на этой

// линии, mtNew = Я не заполнил ни одной клетки на этой линии.

TMoveType = (mtWin, mtBlock, mtOne, mtNew);

var

CurrentMoveType: TMoveType;

function DoCalcMove(CalcType: TCalcType; Position: Integer): Boolean;

var

RowData, I, J, CheckTotal: Integer;

PosVal, Mask: Byte;

begin

Result := False;

RowData := 0;

X := 0;

Y := 0;

if CalcType = ctRow then begin

I := Position;

J := 1;end

else if CalcType = ctColumn then begin

I := 1;

J := Position;

end

else begin

I := 1;

case Position of

1: J := 1; // просмотр из верхнего левого в нижний

// правый угол

2: J := 3; // просмотр из верхнего правого в нижний

// левый угол

else

Exit;              // проверяются только две диагонали

end;

end;

// Mask содержит биты для игрока или компьютера в зависимости

// от тактики нападения или защиты. Checktotal определяет

// строку для хода.

case CurrentMoveType of

mtWin: begin

Mask := PlayerSpot;

CheckTotal := 4;

end;

mtNew: begin

Mask := PlayerSpot;

CheckTotal := 0;

end;

mtBlock: begin

Mask := ComputerSpot;

CheckTotal := 2;

end;

else begin

Mask := 0; CheckTotal := 2;

end;

end;

// просмотр всех линий в текущем типе CalcType

repeat

// Получить статус текущей клетки (X, O или пустая)

PosVal := GameData[I, J];

// Запомнить последнюю пустую клетку в том случае,

// если решено заполнить ее

if PosVal = 0 then begin

X := I;

Y := J;

end else

// Если клетка уже заполнена, добавить маскирующее

// значение в RowData

Inc(RowData, (PosVal and not Mask));

if (CalcType = ctDiagonal) and (Position = 2) then begin

Inc(I);Dec(J);

end else begin

if CalcType in [ctRow, ctDiagonal] then Inc(J);

if CalcType in [ctColumn, ctDiagonal] then Inc(I);

end;

until (I > 3) or (J > 3);

// При увеличении RowData необходимо защищаться или наступать,

// в зависимости от выбранной тактики.

Result := (X <> 0) and (RowData = CheckTotal);

if Result then begin

GameData[X, Y] := ComputerSpot;

Exit;

end;

end;

var

A, B, C: Integer;

begin

if Skill = slAwake then begin

// В первом случае наступать, во втором – защищаться

for A := Ord(mtWin) to Ord(mtBlock) do begin

CurrentMoveType := TMoveType(A);

for B := Ord(ctRow) to Ord(ctDiagonal) do

for C := 1 to 3 do

if DoCalcMove(TCalcType(B), C) then Exit;

end;

// Попытаться занять центральную клетку

if GameData[2, 2] = 0 then begin

GameData[2, 2] := ComputerSpot;

X := 2;

Y := 2;

Exit;

end;

// В противном случае занять самую выгодную позицию на линии

for A := Ord(mtOne) to Ord(mtNew) do begin

CurrentMoveType := TMoveType(A);

for B := Ord(ctRow) to Ord(ctDiagonal) do

for C := 1 to 3 do

if DoCalcMove(TCalcType(B), C) then Exit;

end;

// либо просто найти свободную клетку

for A := 1 to 3 do

for B := 1 to 3 do

if GameData[A, B] = 0 then begin

GameData[A, B] := ComputerSpot;

X := A;

Y := B;

Exit;

end;

end

else begin

// выбирать свободную клетку случайным образом

repeatA := Random(3) + 1; B := Random(3) + 1;

until GameData[A, B] = 0; GameData[A, B] := ComputerSpot; X := A;

Y := B;

end;

end;

procedure TGameServer.CheckCallerSecurity;

begin

// Игра только для членов роли "TTT".

if IsSecurityEnabled and not IsCallerInRole(‘TTT’) then

raise Exception.Create(‘Only TTT role can play tic-tac-toe’);

end;

initialization

Randomize;       // установить генератор случайных чисел

TAutoObjectFactory.Create(ComServer, TGameServer,

Class_GameServer, ciMultiInstance,

tmApartment);

end.

Установка сервера

После  создания сервера его нужно установить в среде  COM+. В Delphi это можно сделать  очень  просто, выбрав  в меню Run пункт Install COM+ Objects. В результате на экране появится диалоговое окно  Install COM+ Objects, при  помощи которого объек ты могут быть установлены в уже существующий или в новый  пакет (рис. 18.17).

end.

На рис. 18.18 показан внешний вид этого  приложения в действии. Человек ставит крестики, а компьютер — нолики.

   

Рис. 18.18. Игра в крестики нолики

Отладка приложений COM+

Компоненты COM+ работают внутри  пространства процесса COM+, а не клиента, поэтому  на первый взгляд может  показаться, что отладка  приложения COM+ должна быть  намного сложнее отладки  обычных приложений. Однако  COM+ содержат спе циальное средство, упрощающее процесс отладки. Загрузите проект сервера, вызови те диалоговое окно Run Parameters (Параметры запуска)  (рис. 18.19) и укажите  в нем в качестве главного приложения  (host  application) mtx.exe. В качестве параметра программе  mtx.exe необходимо  передать   /p:{package guid},   где   package guid —  это  глобальный уникальный идентификатор  пакета,  указанного в  средстве Component Services. Теперь расставьте точки  останова и запустите приложение. Вна чале ничего не произойдет, так как клиентское приложение еще не запущено.  После запуска клиентского приложения произойдет переход в режим  отладки.

Рис. 18.19. Диалоговое окно Run Parameters

Резюме

COM+ — это мощнейшее дополнение к семейству  технологий COM. В данную спе цификацию добавлены службы контроля за временем существования объектов, под держки транзакций и системы безопасности. Кроме  того,  объекты COM+ можно  пре образовывать в объекты COM  без  значительных изменений исходного кода.  Таким образом, корпорацией Microsoft была  разработана  усовершенствованная специфика ция  для разработки распределенных приложений с высокой степенью масштабируе мости. В настоящей главе были рассмотрены основные понятия спецификации COM+ и особенности поддержки COM+ в Delphi. На  основании представленного примера приложения COM+ были изучены некоторые приемы разработки оптимизированных компонентов COM+. Средства COM+ в сочетании с Delphi  предоставляют большие возможности создания масштабируемых многоуровневых приложений. При  этом  не следует  забывать о некоторых различиях между разработкой обычных компонентов COM и компонентов COM+.

Источник: Тейксейра, Стив, Пачеко, Ксавье.   Borland Delphi 6. Руководство разработчика. : Пер.  с англ. — М. : Издательский дом “Вильямс”, 2002. —  1120 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии