Вы - -й посетитель этой странички 

Четыре занятия по ООП
                                                   
        Г.Н. Гутман, г.Самара

Введение для учителя

    Для понимания изложенного в статье материала необходимо иметь опыт работы с типом запись в Паскале так как аналог этой структуры активно используется в объектно-ориентированном программировании. Для выполнения предложенных в статье проектов желательно уметь создавать многомодульные программы, использовать в программах мышь, кнопочные меню и т.д. [1, 2].
    Материал статьи разбит на четыре занятия, каждое из которых, однако, может потребовать от двух до четырех обычных уроков. Это время будет потрачено не зря, если на основе разработанного набора объектов создается сразу несколько программ, например, после разбора примера-проекта "Игра "15" рекомендуется предложить каждой группе учеников разработку одного из предложенных в задании проектов. В этом случае легко организовать перекрестное тестирование и обсуждение работ и в завершение — защиту проектов.

Занятие 1. Инкапсуляция

    До сих пор, составляя сложные программы на Паскале, мы оставались в рамках процедурного подхода. Основная задача разбивалась на ряд более простых задач, каждая из которых могла быть оформлена в виде отдельной процедуры или функций. При этом подходе больше внимания уделяется разработке алгоритма и существенно меньше — используемым структурам данных.
    В настоящее время для разработки сложных программ все больше применяется объектно-ориентированный подход. Основная его идея — объединение данных и алгоритмов их обработки в единое целое — объект. Объект имеет состояние (например, размер, цвет и т.д.) и методы (действия, на которые способен объект). Чтобы заставить объект что-то сделать, нужно послать ему соответствующее сообщение.
    В Турбо Паскале объект — это особый тип данных, а экземпляры объекта — переменные этого типа. Состояние объекта характеризуется значениями полей; методы объекта — связанные с ним процедуры и функции, которым доступны поля. Передача сообщений экземпляру объекта происходит в виде вызовов его методов с заданными параметрами.
    Инкапсуляция — это объединение описаний структуры данных с процедурами и функциями, работающими с полями этой структуры, которое формирует новый тип данных — объект.
   
При этом данные одного объекта недоступны методам другого объекта, если эти объекты не связаны друг с другом.
    Проиллюстрируем сказанное рисунком.
                                           

    Синтаксис описания объектного типа в Паскале:

tуре имя_типа = objeсt
объявление данных
объявление методов
end;

    Легко видеть, что описание объектного типа похоже на описание записи, только здесь добавлены объявления методов, т.е. процедур и функций для работы с данными объекта. Однако объявление метода является фактически его заголовком, описания методов располагаются вне блока type. При этом имя метода рассматривается как поле объектного типа, поэтому снабжается указанием имени типа. Приведем описание метода-процедуры:

procedure имя_типа.имя_метода(параметры);
const . . .
var . . .
begin
    <тело процедуры>
end;

    Аналогично описывается метод-функция.

    Пример

type TPoint = object
    x,
у : integer
    procedure Init (x0,
у0 : integer) ;
end;
procedure TPoint.Init (x0,
у0 : integer) ;
begin
    x:=x0;
    y:=y0
end;

    Объект TPoint описывает точку с целочисленными координатами на плоскости OXY. Метод Init присваивает значения координатам точки, т.е. инициализирует экземпляр объекта. Обычно этот метод присутствует в каждом объектном типе и вызывается первым.

    Задание 1. Добавьте в TPoint метод Equal, кoтoрый проверяет совпадение экземпляра объекта с заданной точкой (т.е. устанавливает совпадение их координат).
    Решение. Добавим в описание типа TPoint объявление метода:

function Equal (A : TPoint) : boolean;

а вне типа опишем саму функцию:

function TPoint.Equal (А : TPoint): boolean;
begin
    Equal: = (x=A. x) and (y=A.y)
end;

    Замечание 1. x и А.х (аналогично у и А. у) — это разные переменные, поэтому никакой путаницы не происходит.

    Задание 2. Добавьте в TPoint новый метод — процедуру, которая присваивает экземпляру объекта значения другой точки, передаваемой методу как параметр.

    Переменные объектного типа, как и любые другие переменные, объявляются в разделе var. Можно объявить как простую переменную, так и массив:

const n=6;
var S : TPoint,
    
А : array [l . . n] of TPoint;

    Точно так же объектный тип можно использовать в качестве поля записи, например, можно ввести тип ТSegment — отрезок на плоскости:

type TSegment = object
   
А, В : TPoint;
    procedure Init (x1, y1, x2, y2 : integer) ;
end;
procedure TSegment.Init (x1, y1, x2, y2 : integer) ;
begin
    A.x: =x1; A.
у: =y1 ;
    B.x:=x2;  B.y:=y2;
end;

    Доступ к методам объектного типа аналогичен доступу к его данным. Синтаксис вызова метода в общем. случае выглядит следующим образом:

имя_переменной.имя метода (список параметров).

    Для инициализации, например, нулевыми значениями объявленной в разделе var переменной S необходимо написать S.Init (0,0).

    Задание 3. Ввести координаты трех вершин треугольника и вычислить координаты его центра тяжести.

    Указание. Координаты центра тяжести треугольника ABC вычисляются по формулам:

xc=1/3 (Ax + Bx + Cx)
yc=1/3 (Ay + By + Cy)

(индексами х и у отмечены соответствующие координаты вершин треугольника).

Занятие 2. Наследование

    Предположим, мы хотим дополнить уже разработанный объектный тип новыми свойствами. Можно, конечно, его переделать, но объектно-ориентированный подход рекомендует создать новый объектный тип (объект-наследник), который является расширением уже имеющегося базового типа. Новый тип будет включать в себя все данные и методы базового типа. И при этом может иметь собственные поля, собственные методы и перекрывать своими методами одноименные унаследованные методы. Таким образом, в ООП работает механизм наследования — такого "отношения между объектами, когда один объект повторяет структуру и поведение другого" (Гради Буч). Базовые объекты называют родителями и предками, а объекты-наследники — потомками. В результате реализации механизма наследования между объектами возникает иерархия.
   
Добавим в разработанный нами тип TPoint возможность вывода точки на экран дисплея. Достаточно ли для этого ее координат х и y? Если мы хотим использовать цвет при выводе точки, то в новый класс следует добавить соответствующий элемент данных (color). Кроме этого, добавятся методы, с помощью которых можно "показать" (метод Show) и "спрятать" (метод Hide) точку.

    Синтаксис объявления объекта-наследника (потомка):

type имя_типа = оbjeсt(имя_базового_типа)
   
объявление дополнительных данных
   
объявление дополнительных методов
end;

Назовем новый тип Т Pixel.

type TPixel == object (TPoint)
    color : word;
    procedure Init (x0, y0, c0 : word);
    procedure Show;
    procedure Hide;
end;

    Еще раз подчеркнем, что TPixel содержит и координаты точки (х, y), так как наследует их из типа TPoint.
    Процедуру TPixel.Init можно написать по аналогии с TPoint.Init:

procedure TPixel.Init (х0, у0, с0 : word) ;
begin
    х:=х0;
    у:=у0;
    color:=с0
end;

но лучше воспользоваться тем, что большую часть работы можно поручить соответствующей процедуре базового типа. Чтобы при этом компилятору было ясно, какая из процедур имеется в виду, имя метода снабжается указанием имени объектного типа:

procedure TPixel.Init (х0, у0, с0 : word);
begin
    TPoint.Init (
х0, у0) ;
    color:=
с0
end;

    Тот же эффект достигается, если вместо TPoint.Init (x0, y0) написать inherited Init (x0, y0).

Слово inherited означает, что вызывается метод базового типа (предка), непосредственно от которою произошел данный тип (потомок).
    Приведем без комментария тексты методов Show и Hide.

procedure TPixel.Show;
begin
    putpixel (x, y, color)
end;

procedure TPixel.Hide;
begin
putpixel (x, y, getbkcolor)
end;

    Задание 1. Напишите программу, которая случайным образом "зажигает" и "гасит" на экране разноцветные точки.

    Указание. Объявите в программе массив точек как экземпляры объекта типа TPixel, инициализируйте их случайными данными. Используйте методы Show и Hide для случайно выбранных элементов массива.

    Задание 2. Нарисуйте окружность движением точки, затем сотрите ее.

    Указание. Используйте массив точек, расположенных по окружности.

    Добавим метод, который перемещает точку в другую позицию экрана (при этом цвет точки не изменяется). Чтобы переместить изображение точки, ее следует стереть с экрана и вывести снова, но уже в другой позиции. Для этою удобно использовать уже имеющиеся процедуры Show и Hide. Заметим, что, кроме внешнего действия, метод должен изменить и значения координат точки.

procedure TPixel.Move (newX, newY : integer) ;
begin
    Hide;
    x:=newX;
у:=newY;
Show;
end;

    Задание З. Напишите метод ChangeC, который изменяет цвет точки без изменения ее положения на экране.

    Задание 4. Используя методы Move и ChangeC, напишите Программу движения точки по некоторой прямой. Точка должна изменять цвет, когда проходит над заданной областью экрана.

    Задание 5*. Смоделируйте "движение через Вселенную":, светящиеся точки-звезды разбегаются от центра экрана к его границам, вместо исчезнувших в центре экрана появляются новые точки-звезды.

Занятие З. Полиморфизм

    Продолжим построение иерархии объектов. Создадим тип TCircle (окружность), который сделаем производным от типа TPixel (тип TCircle — потомок типа TPixel). Поскольку новый тип наследует данные и из TPixel, и из TPoint, то в нем уже содержатся поля x, у и color. Осталось только добавить радиус окружности R и метод ChangeR для его изменения:

type TCircle = object (TPixel)
    R : word;
    procedure Init (x0, y0, r0, c0 : word);
    procedure Show;
    procedure Hide;
    procedure ChangeR (newR : integer);
end;

procedure TCircle.Init (x0, y0, r0, c0 : word) ;
begin
    {
вызываем метод базового типа}
    inherited Init (x0, y0, c0) ;
    R:=r0;
end;

procedure TCircle.Show;
begin
    SetColor (color) ;
    Circle (x, y, R) ;
end;

procedure TCircle.Hide;
begin
    SetColor (getbkcolor) ;
    Circle (x, y, R) ;
end;

procedure TCircle.ChangeR (newR: integer) ;
begin
    if newR>=0 then begin
        Hide;
        R:=newR;
        Show;
    end;
end;

    Методы Move и ChangeC для окружности текстуально совпадают с этими же методами для точки. Можно ли наследовать их из базового типа? Попробуем заменить объект типа TPixel на объект типа TCircle в одной из предыдущих программ. Пока мы используем только методы Show и Hide, программа работает нормально. Как только мы пытаемся сместить окружность методом Move (или измерить ее цвет методом ChangeC ), окружность превращается... в точку! Разберемся, почему это происходит.
    Когда метод TPixel.Move компилируется, то в него вставляются вызовы методов TPixel.Show и TPixel.Hide, поэтому даже если метод Move вызывается из объекта типа TCircle, изображаться на экране будет все же точка, а не окружность.
    Решить эту проблему можно двумя способами. Первый — это включить метод Move (а также ChangeC) в тип TCircle. Это решение нельзя назвать удачным, так как при последующем добавлении в иерархию новых объектов придется каждый раз переписывать целую кучу методов.
    Второй способ — объявить методы Show и Hide, от которых и зависит, что будет нарисовано на экране, виртуальными. Тогда при компиляции метода ТPixel.Move в него вместо вызова метода TPixel.Show вставляется ссылка на таблицу, содержащую адреса всех методов с именем Show (так называемую таблицу виртуальных методов). Метод, объявленный виртуальным в некотором объекте, обязан быть виртуальным и во всех производных объектах. На этапе выполнения программы при вызове метода Show из таблицы будет взят адрес того из них, который принадлежит объекту, вызвавшему этот метод.
    На этом примере мы познакомились с третьим свойством ООП — полиморфизмом. Полиморфизм — свойство различных объектов выполнять одно и то же действие (метод) по-своему.
    Таким образом, если переменная С объявлена как точка, то вызов С.Move нарисует на новом месте точку, а если переменную С объявить как окружность, то вызов С.Move нарисует на новом месте окружность.

    При объявлении виртуальных методов (т.е. внутри типа) в конце заголовка добавляется слово virtual, например:

    procedure Show; virtual;
    procedure Hide; virtual;

    На самом деле требуется еще одно существенное изменение. Если в описании типа есть виртуальный метод, то каждый экземпляр этого типа должен содержать указатель на таблицу виртуальных методов. Этот указатель экземпляр типа может получить только тогда, когда он будет создан, т.е. уже при выполнении программы. Обычно эти действия "поручаются" процедуре инициализации объекта, так как она вызывается первой, до вызова любого виртуального метода. Чтобы компилятор добавил соответствующий код к процедуре инициализации, следует объявлять и описывать ее с ключевым словом constructor, т.е. слово procedure заменяется словом constructor.
    Каждый отдельный экземпляр объекта должен инициализироваться отдельным вызовом конструктора. Недостаточно инициализировать один экземпляр объекта и затем присваивать этот экземпляр другим. Это может привести к зависанию системы при попытке использовать их виртуальные методы. Ниже приведен пример с ошибкой:

var А, В : TPixel;
begin
    { инициализация графики }
    A.Init (50, 90, 5) { вызов конструктора для А }
    В := А; { ошибка! }
    В.Show; { зависание системы }
    . . .
end;

    Задание 1. Используя виртуальные методы и конструкторы, напишите программу движения окружности по некоторой прямой. Окружность должна изменять свой цвет при прохождении ее центра над заданной областью экрана.

    Задание 2. Добавьте в иерархию объектных типов новый тип — шестиугольник (тип THexagon), причем значение Tcircle.R равно радиусу описанной вокруг шестиугольника окружности.

    Задание 3. Добавьте в иерархию объектных типов новый тип — эллипс (тип TEllipse), причем значение Tcircle.R равно величине горизонтальной полуоси эллипса (для вертикальной полуоси потребуется ввести новый элемент данных). Напишите новый метод, который изменяет величину вертикальной полуоси независимо от горизонтальной.

Занятие 4. Обработка событий в объекте

    Большинство современных программ предполагают интерактивный режим работы. В этом случае все действия программы являются ответом на те или иные действия пользователя — программа реагирует на внешнее событие: нажатие клавиши на клавиатуре, кнопки на мышке, перемещение мышки и т.п.
    Обработкой всех этих событий при традиционном программировании обычно занимается главная программа. Объектно-ориентированный подход предполагает создание в каждом из объектных типов специальной процедуры HandleEvent — обработчика событий. Тем самым "убиваются два зайца": во-первых, сохраняется принцип инкапсуляции и, во-вторых, упрощается доступ к элементам данных и методам объекта.
    Что же остается в этом случае на долю главной программы? Так называемый цикл опроса клавиатуры и вызов обработчика событий для конкретного объекта.
    Включим метод HandleEvent в объявление типа TPixel и для простоты в качестве событий будем рассматривать только ввод с клавиатуры:

    procedure HandleEvent (code : word) ;

    Параметр code — это код нажатой на клавиатуре клавиши.

procedure TPixel.HandleEvent(code : word) ;
begin
    case code of
        LF: Move (x-step, y) ;
        RT: Move (x+step, y) ;
        UP: Move (x, y-step);
        DN: Move(x, y+step) ;
        Ord('1'): ChangeC(1) ;
        Ord('2'): ChangeC(2) ;
        Ord('3'): ChangeC(3) ;
        Ord('4'): ChangeC(4) ;
        Ord('5'): ChangeC(5) ;
       
Оrd('6'): ChangeC(6) ;
        Ord('7'): ChangeC(7) ;
end;

    Константы LF (влево), RT (вправо), UP (вверх), DN (вниз) скрывают значения, возвращаемые функцией Rdkey при нажатии на клавиши управления  ,® ,¬ , ¯.

function Rdkey : word;
var kod : word;
begin
    kod:=0rd (Readkey) ;
    if kod=0 then kod:=256+0rd (Readkey) ;
    Rdkey:=kod
end;

    Эти значения и другие, которые могут понадобиться при анализе, должны быть описаны в разделе const главной программы:

const
    LF=331;
    RT=333;
    UP=328;
    DN=336;
    Esc=27;
.  .  .

    Задание 1. Напишите метод HandleEvent для объектного типа ТCircle.

    Указание
    Непосредственнов методе TCircle.HandieEvent проанализируйте только клавиши, назначенные для изменения цвета окружности. Во всех остальных случаях поручите это методу TPixel.HandleEvent.

    Приведем полностью программу "Управление окружностью".

uses Crt, Graph;
const
    LF=331;
    RT=333;
    UP=328;
    DN=336;
    Esc=27;
var step : word;
{----------------------- Rdkey -----------------------}
function Rdkey : word;
var kod : word;
begin
    kod:=0rd (Readkey) ;
    if kod=0 then kod:=256+0rd(Readkey) ;
    Rdkey:=kod;
end;
{----------------------- Tpoint -----------------------}
type TPoint = object
    x,
у : integer;
    procedure Init (x0, y0 : integer) ;
end;
{----------------------- TPixel -----------------------}
type TPixel = object (TPoint)
    color : word;
    constructor Init (x0, y0, c0 : integer);
    protedure Show; virtual;
    procedure Hide; virtual;
    procedure Move (newX, newY: integer) ;
    procedure ChangeC (newC: word) ;
    procedure HandleEvent (code : word);
virtual;
end;
{----------------------- TCircle -----------------------}
type TCircle = object (TPixel)
    R : word;
    constructor Init (x0, y0, r0, c0 : word) ;
    procedure Show; virtual;
    procedure Hide; virtual;
    procedure ChangeR (newR: integer);
    procedure HandleEvent (code : word);
virtual;
end;
{--------------------------------------------------------}
procedure TPoint.Init (x0, y0 : integer) ;
begin
    x:=x0; y:=y0;
end;

constructor TPixel.Init(x0, y0 c0 : integer;
begin
   {
вызываем метод базового типа}
     inherited Init(x0, y0) ;
    color:=c0;
end;
procedure TPixel.Show;
begin
    putpixel (x, y, color)
end;
procedure TPixel.Hide;
begin
    putpixel (x, y, getbkcolor)
end;
procedure TPixel.Move (newX, newY: integer);
begin
    Hide;
    x:=newX; y:=newY;
    Show
end;
procedure TPixel.ChangeC(newC: word) ;
begin
    color:=newC;
    Show;
end;
procedure TPixel.HandleEvent (code : word) ;
begin
    case code of
        LF: Move (x-step, y) ;
        RT: Move (x+step, y) ;
        UP: Move (x, y-step) ;
        DN: Move (x, y+step);
        Ord( '1') : ChangeC (1) ;
        Ord('2') : ChangeC (2) ;
        Ord ('3') : ChangeC (3) ;
        Ordt ('4') : ChangeC (4) ;
        Ord ('5') : ChangeC (5) ;
         Ord ('6') : ChangeC (6) ;
        Ord ('7') : ChangeC (7) ;
    end;
end;
constructor TCircle.Init (x0, y0, r0, c0 : word) ;
begin
    {
вызываем метод базового типа}
    inherited Init (x0, y0, c0) ;
    R:=r0;
end;
procedure TCircle.Show;
begin
    SetColor (color) ;
    Circle (x, y, R);
end;
procedure TCircle.Hide;
begin
    SetColor (getbkcolor) ;
    Circle (x, y,R);
end;
procedure TCircle.ChangeR (newR: integer);
begin
    if newR>=0 then begin
        Hide;
        R:=newR;
         Show;
    end;
end;
procedure TCircle.HandleEvent (code : word);
begin
    case code of
        Ord ('+') : ChangeR (R+step) ;
       Ord ('-') : ChangeR (R-step) ;
        else inherited HandleEvent (code) ;
    end;
end;
{------------- main -------------}
var  Gd, Gm, errCode : integer;
        ch : word;
        C: TCircle;
begin
    {----
инициализация графики -----}
    Gd:=-DETECT;
    InitGraph (Gd, Gm, '');
    errCode := GraphResult;
    if errCode <> grOK then begin
        writeln (GraphErrorMsg (errCode) ) ;
         exit;
    end;
    {--------------------------------------}
    step:=5;
    C.Init (100, 100, 20, 3) ;
   
С.Show;
    repeat
        ch:=Rdkey;
        C.HandleEvent (ch)
    until ch=Esc;
    {--------------------------------------}
    CloseGraph;
end.

    Задание 2. Измените методы TCircle.Show и Tcircle.Hide так, чтобы обеспечить управление окружностью белого цвета, за которой остается цветной след. Цвет следа можно изменять нажатием тех же клавиш, что и в написанной выше программе.

    Задание 3. Разработайте новый тип, который является производным от одного из объектных типов иерархии (квадрат, прямоугольник, эллипс, закрашенная окружность, шестиугольник и т.д.).

    Задание 4. Разработайте новый, не входящий в иерархию, тип, который в качестве элементов данных содержит два (или более) объекта иерархии. Например, "кольцо" — две концентрические окружности, "гайка" — шестиугольник с отверстием, "пуговица", "рожица" и т.д.

    Проект "Игра "15"

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15  


    Используем созданные объектные типы при написании программы игры-головоломки. Придуманная Сэмом Лойдом, она была очень популярна в семидесятых годах прошлого века, впрочем, и в наши дни дети и многие взрослые с удовольствием играют в эту игру.
    "Игра "15" представляет собой 15 фишек, на которых написаны числа от 1 до 15, уложенные в коробочку в случайном порядке. Нужно, передвигая фишки на свободное место, но не вынимая их из коробочки, расставить, так, как показано на рисунке.
    Основным объектом в нашей программе будет квадратная фишка с надписью (надпись может быть только записью целого числа). Опишем этот объект как тип TSquare. Понадобятся также координаты точки, определяющей положение фишки, ее цвет и размер. Чтобы не писать новый объект "с нуля", объявим его потомком типа TCircle, тогда останется только добавить поле text и написать процедуры Show и Hide.

type TSquare = object (TCircle)
    text : string;
    constructor Init (x0, y0, a0, c0 : word; text0 : string);
    procedure Show; virtual;
    procedure Hide; virtual;
end;
constructor TSquare.Init (x0, y0, r0, c0 : word; text0: string) ;
begin
   
TCircle.Init (x0, y0, r0, c0) ;
    text:=text0
end;

                                           

    Сама фишка — это закрашенный цветом color квадрат с белой рамкой и белым текстом. Значения х, у — координаты центра квадрата, а его сторона равна 2R.
    Напишем для нового типа обязательные процедуры рисования и стирания объекта. Стирать фишку будем квадратом того же цвета, но с, другим стилем заполнения ("сеточкой"):

procedure TSquare.Show;
begin
    {рисуем фон фишки}
    SetFillStyle (1, color) ;
    bar (x-R,y-R,x+R,y+R);
    {
рисуем рамку}
    SetColor (15);
   
rectangle (x-R, y-R, x+R, y+R);
   
{выводим текст)
     SetTextStyle (0, 0, R div 8);
   
SetTextJustify (CenterText, CenterText) ;
    OutTextXY(x, y, text)
end;
procedure TSquare.Hide;
begin
    SetFillStyle (8, color);
    bar (x-R, y-R, x+R, y+R)
end;

    Все остальные процедуры (в том числе и обработчик событий HandleEvent) будем наследовать из базового типа.

    Теперь займемся написанием главной программы. Коробочка, в которую уложены фишки, моделируется двухмерным массивом. Начальная расстановка может выбираться случайным образом, но тогда примерно в половине случаев головоломка не будет иметь решения. Попросите ваших учеников объяснить этот факт. В качестве подсказки предложите им "нерасставляемый" вариант и попросите их проанализировать исходное состояние фишек. Удобнее использовать другой подход: случайными передвижениями фишек нарушить начальную расстановку.
    Итак, объявим в главной программе двухмерный массив фишек:

    Р: array [1..4, 1..4] of TSquare;

и инициализируем его:

size:=38; step:=size+2;
for i:=1to 4 do begin
    for j:=1 to 4 do begin
        x:=100+(j-1) *step;
        y:=100+ (i-1) *step;
        if (i=4) and (j=4) then begin
   
           P[i, j].Init (x, y, size div 2, 7,'');
            P[i, j].Hide end
        else begin
            k:=4* (i-1)+j;
            Str (k, sym) ;
            P[i, j].Init (x, y, size div 2, 7, sym);
            P[i, j].Show
        end
    end
end;

    Напишем основной цикл получения события от клавиатуры и его обработки. Главная задача — понять, к какой фишке относится событие ("команда"), полученное с клавиатуры. Теоретически пользователь должен сначала выбрать фишку ("дотронуться" до нее), а потом уже передвинуть ее. Но в данном случае задача упрощается, так как в любой ситуации есть не более одной фишки, которая может двигаться в заданном направлении. Эта фишка (поле с индексами i, j) находится в противоположном заданному направлении от пустой клетки поля (ее индексы мы обозначим через i0, j0).

i0:=4; j0:=4;
i:=i0; j:=j0;
repeat
    flag:=true;
    ch:=Rdkey;
   
{ анализируем нажатую клавишу }
    case ch of
        LF: if j0<4 then j:=j0+1;
   
     RT: if j0>1 then j:=j0-1;
        UP: if i0<4 then i:=i0+1;
        DN: if i0>1 then i:=i0-1;
        else flag:=false
    end;

if flag then begin
        P[i, j].HandleEvent (ch) ;
        P[i0, j0]:=P[i, j];
        i0:=i; j0:=j
    end
   
{ выход из цикла - по клавише Esc }
until ch=Esc;

    Флаг блокирует вызов обработчика событий и в том случае, если нажата "посторонняя" клавиша, и в том случае, если ни одна фишка в данном направлении двигаться не может. Кроме перемещения фишки, мы обязаны также обновить информацию о рабочем поле.
    Для получения начальной расстановки используем уже написанный цикл. Постараемся несколько раз "подсунуть" программе случайно выбранное событие — "команду" перемещения фишки. Чтобы не сильно удлинять текст программы, используем значение переменной nwsp (количества начальных перемещений фишек) в качестве критерия выбора. Ниже приведен фрагмент программы, который потребуется добавить в основной цикл вместо команды ch: =Rdkey.

if nwsp>0 then begin
   
{
случайный выбор }
    ch:=Random(4);
    case ch of
    0: ch:=lf;
        1: ch:=rt;
        2: ch:=up;
        3: ch:=dn;
    end;
   
nwsp:=nwsp-1;
end else ch:=Rdkey; {ввод "команды" с клавиатуры}

    Понятно, что до начала цикла необходимо инициализировать переменную nwsp и датчик случайных чисел.

    Задание 1. Напишите программу игры целиком. Сделайте заставку, оформите рабочее поле, выведите на него информацию о количестве ходов и затраченном времени.

    3адание 2. Задайте начальную расстановку в текстовом файле. Устройте соревнование — кто быстрее решит эту головоломку.

    Задание 3. Модифицируйте программу игры так, чтобы вместо чисел на фишках появлялись буквы некоторой фразы, которая заранее сообщается игроку. Тогда, решив головоломку, мы сможем ее прочитать.

    Замечание. Если в придуманной фразе некоторые символы совпадают, решение головоломки содержит ловушку: если один из совпадающих символов поставить не на свое место, то решить задачу не удастся. Поэкспериментируйте, например, с такой фразой: "СЛОН СПИТ СТОЯ, А ВЫ?" (но без знаков препинания и пробелов):

с л о н
с п и т
с т о я
а в ы  


    Задание 4.
Разработайте свой проект, в котором бы использовались объекты. Найдите в научно-популярной литературе, например в замечательной книге М.Гарднера [3], игры, в которых используются фишки, и попробуйте реализовать их.

Варианты проектов

1. Конструктор слов

    На экран выводится прямоугольная таблица с буквами, а рядом — поле для набора слов. При наборе на клавиатуре буква, которая есть в таблице, исчезает из таблицы и появляется в набираемом слове. Задача — собрать несколько слов (фразу), используя все буквы таблицы.

2. Вращающиеся квадратики

    На экране появляется квадратная или прямоугольная таблица, заполненная числами. Любой квадратик из 4 соседних клеток таблицы можно неоднократно поворачивать на 90° в любую сторону. Задача — расположить числа в таблице в порядке возрастания.
    Выделять квадратики можно двигающейся рамкой — графическим курсором (этот объект легко встраивается в разработанную иерархию), а поворот выполнять при нажатии каких-либо соседних клавиш.
    Можно создать и вариант с буквами (или частями слов) вместо цифр, тогда в результате должна получиться заданная фраза (например, "БЫТЬ ИЛИ НЕ БЫТЬ"). Для лучшей читаемости текущее содержание таблицы можно отображать в строке на экране.

3. Восемь полосок

    На экране появляется квадратная таблица размером 4х4 клетки, заполненная числами. Любой столбик или строку из 4 клеток таблицы можно сдвигать на одну клетку вверх или вниз (строку — влево или вправо). При этом числа будут циклически передвигаться из одной клетки таблицы в другую. Задача — расположить числа в таблице в порядке возрастания.
    Для выбора строки (столбца) можно разработать два вида указателей. Движение указателей наследуется из разработанной иерархии, переключение между ними — клавишей, движение полосок — стрелка.
    В идеале следовало бы создать указатели, реагирующие на нажатие кнопок мыши. Тогда по периметру таблицы можно расположить 16 кнопок со стрелками, указывающими возможные направления движения каждой полоски, и нажимать на них курсором мыши.

4. Перевертыши

  

 
   
 
     
 
 


    На экране появляется квадратная таблица размером 4х4 клетки. В каждой клетке находится кружок. Одна сторона кружка — синяя, другая — белая. Начальное распределение цветов можно считать случайным. За один "ход" можно перевернуть кружки в какой-либо произвольно выбранной клетке и во всех соседних с ней по вертикали и горизонтали клетках (в зависимости от расположения выбранной клетки надо будет переворачивать от 3 до 5 клеток). Задача головоломки — получить во всех клетках один и тот же цвет кружков.

    Указание. Для обозначения выбранной клетки удобно использовать рамку, которую можно перемещать клавишами-стрелками. Для работы с двухцветными кружками разработайте новый тип — TFillCircle, для изменения цвета добавьте в этот тип процедуру Reverse.

5. Холодильник братьев Пилотов

    На экране появляется квадратная таблица размером 4х4 клетки, в каждой клетке находится переключатель. Каждый из переключателей может находиться только в двух положениях — горизонтальном или вертикальном. При повороте одного переключателя меняют свое состояние все переключатели, расположенные во всех клетках той же строки и того же столбца. Можно ли добиться того, что во всех клетках таблицы переключатели будут находиться в одном и том же положении?

    Замечание. Два положения переключателей можно моделировать квадратными фишками с цифрами 0 и 1 или же двухцветными кружками, как в предыдущем проекте.

6. Спирограф
   
Спирографом называется нехитрый прибор, с помощью которого можно строить различные кривые. Эти кривые оставляет след карандаша, вставленного в одно из отверстий на круге, который катится по неподвижной окружности.
    Его модель можно сделать, добавив в иерархию новый тип — точку, оставляющую при движении за собой след. Однако этот проект требует хорошей математической подготовки.

7. Модели многогранников
   
Модель многогранника представляет собой набор вершин, некоторые из которых соединены друг с другом линиями. Многогранник — это пример объекта- "контейнера", который содержит в себе объекты иерархии, но сам не входит в нее. Следовательно, для него нужно будет написать все необходимые методы (движение в разных направлениях, повороты вокруг вертикальной и горизонтальной осей и т.д.).
    И этот проект требует очень хороших математических знаний: необходимо вычислять координаты вершин многогранника в пространстве (это легко только для куба), "переводить" их на плоскость, знать формулы поворота и т.д.
    Ряд проектов разной сложности ("Таблица чисел", "Тест на тренировку памяти", "Исполнитель Муравей", "Докер", "Клавиатурный тренажер" и т. д.) можно найти в статье автора [4].

Рекомендуемая литература

    1. Фаронов В.В. Основы Турбо Паскаля. М.: МВТУ-ФЕСТО ДИДАКТИК, 1992, 304 с.
    2. Мизрохи С.В. Turbo Pascal и объектно-ориентированное программирование. М.: Финансы и статистика, 1992, 190 с.
    3. Гарднер М. Математические головоломки и развлечения. М.: Мир, 1971, 511 с.
    4. Гупгман Г.Н. Мой любимый QuickBasic. Выпуск 5. "Информатика", № 42/2000.