Разработка приложений на языке Visual Prolog

Игра Крестики-нолики

Показывать лекцию целиком

Правила игры. Стратегия выбора хода

Реализуется следующий вариант игры "Крестики-нолики". Игра ведется на прямоугольном поле размером m х n, где $$$m\geq3$$$ и $$$n\geq3$$$, но место для игры (игровой участок) не должно выходить за пределы квадрата, содержащего девять клеток (3 х 3). В отличие от обычной игры, этот участок определяется только в процессе игры. Выигрывает игрок, поставивший в ряд по вертикали, по горизонтали или по диагонали три своих камня. Если участок заполнен полностью, а ни одному игроку не удалось поставить три камня в ряд, то признается ничья.

Первый игрок первым ходом может поставить камень в любое поле. Игроки ходят по очереди. Они могут ходить только в доступные поля, так чтобы в результате все камни оказались на участке размером 3 х 3. Например, если первый игрок поставит камень так, как показано на рис. 9.1 ( , то второй игрок может ходить в любое поле, находящееся внутри выделенного квадрата, доступными для него являются 24 поля. После хода второго игрока, показанного на рис. 9.1 ( , место игры сужается, доступными для первого игрока остаются 14 полей. Если первый игрок пойдет так, как показано на рис. 9.1 ( , то место игры будет полностью определено.

(рис 9.1) Определение игрового участка

Нетрудно заметить, что у первого игрока больше шансов выиграть. Тем не менее правильная игра, т. е. игра, в которой оба игрока ходят наилучшим для себя образом, всегда завершится вничью.

Если игрок ставит камень в ряд, в котором уже имеются два камня противника, то такой ход называется блоком, он предотвращает немедленный выигрыш противника. Вилкой называется ход, после которого возникает два различных ряда, содержащих по два камня игрока и не содержащих камней противника; на пересечении этих рядов должен стоять камень. Если участок игры будет полностью определен, то противник сможет заблокировать только один из таких рядов, поэтому у игрока останется возможность поставить камень третьим в другой ряд и выиграть. Если же участок игры не будет полностью определен, то и выигрыш, вообще говоря, не будет предопределенным. Поэтому вилкой в данной игре будет называться только такой ход, после которого участок игры будет полностью определен.

Ниже перечисляются правила, в соответствии с которыми ходит игрок-компьютер. Они приводятся в том же порядке, в котором реализуются в программе. Формулировка правил — "декларативная", по возможности самая общая, без уточнения всех случаев (см. программу). Правила реализуются просто и в общем виде за счет использования списков. Списки в языке Пролог — это сохраняющий (persistent) тип данных, поэтому их удобно использовать для просчитывания позиции на шаг или на несколько шагов вперед.

1. Если противник сделал первый ход, то игрок ставит камень в соседнее по диагонали поле.

После первого хода первого игрока, второй игрок имеет, самое большее, пять вариантов хода, с точностью до симметрии (см. рис. 9.1 ( , где они отмечены точками). В четырех из них он проиграет в игре с правильным игроком-противником (см. ниже правило 6). Остается ход по диагонали в соседнее поле. В программе такое поле выбирается случайным образом.

2. Игрок ставит свой камень третьим в ряд и выигрывает.

3. Игрок ставит блок так, чтобы противник не мог в ответ поставить вилку или поставить камень третьим в ряд.

На рис. 9.2 ( приведен пример позиции, в которой применяется правило 3. Ходит игрок темными. Он должен поставить камень в поле, находящееся непосредственно слева или справа от двух светлых камней, в противном случае он немедленно проиграет. Если игрок поставит камень слева от камней противника, то тот сможет сразу поставить вилку и выиграть. Поэтому игрок должен ходить так, как показано на рис. 9.2 (.

(рис 9.2) Блок с предотвращением вилки, ходит игрок темными

4. Если участок игры полностью определен, то игрок ставит блок.

5. Игрок ставит вилку.

6. Игрок делает такой ход, чтобы противник был вынужден поставить блок, так чтобы следующим ходом игрок мог поставить вилку.

На рис. 9.3 ( рис 9.6 приведены примеры позиций, в которых для выбора хода игрока применяется правило 6. Ходит игрок темными. Правильные ходы показаны на рис. 9.3 ( рис 9.6. В каждом случае противник вынужден ставить блок, после чего игрок ставит вилку (рис. 9.3 ( рис 9.6 ).

(рис 9.3) Принуждение к блоку с последующей вилкой. Позиция 1 (рис 9.4) Принуждение к блоку с последующей вилкой. Позиция 2 (рис 9.5) Принуждение к блоку с последующей вилкой. Позиция 3 (рис 9.6) Принуждение к блоку с последующей вилкой. Позиция 4

7. Если участок игры полностью определен, то делается ход в центральное поле.

На рис. 9.7 ( приведен пример позиции, в которой игрок темными ходит в центральное поле игрового участка (рис. 9.7 ( . Нетрудно заметить, что если он сделает любой другой ход, то противник сможет поставить ему вилку.

(рис 9.7) Ход в центральное поле

8. Если центральное поле занято, то игрок ходит так, чтобы участок игры был определен, и при этом противник ответным ходом не мог поставить вилку.

На рис. 9.8 ( приведен пример позиции, в которой игрок темными выбирает ход по правилу 8 (рис. 9.8 (. Если он пойдет в угловое поле игрового участка, то противник сможет поставить ему вилку.

(рис 9.8) Предотвращение вилки противника

9. Игрок занимает поле, ходом в которое противник поставил бы ему вилку.

10. Игрок делает случайный ход в доступное поле (последние два правила программой фактически не используются).

Создание игрового поля

Создадим проект tictactoe (MDI). После запуска приложения должно открыться окно установки параметров, в котором пользователь сможет указать размеры поля, игрока, который будет ходить первым, и цвет камней.

Создание окна установки параметров

С помощью диалогового окна Create Project Item создадим диалоговое окно settingsDialog.

(рис 9.9) Диалоговое окно установки параметров

Разместим в нем следующие элементы управления (рис. 9.9 рис 9.9):

  • надписи (Static Text) "Ваше имя", "Количество строк", "Количество столбцов", "Ходит первым";
  • поле редактирования (Edit Control):
  • Name: name_ctl; Text: Петя (введите свое имя);
  • пользовательские элементы управления (Custom Control; Class: integerControl):
  • Name: row_ctl; Text: 15; Maximum: 30; Minimum: 3;
  • Name: column_ctl; Text: 15; Maximum: 30; Minimum: 3;
  • флажок (Check Box):
  • Text: компьютер;
  • групповой блок (Group Box):
  • Text: Цвет ваших камней;
  • в групповом блоке нужно разместить два переключателя (Radio Button):
  • Name: dark_ctl; Text: темный; RadioState: checked;
  • Name: light_ctl; Text: светлый.
  • Теперь следует включить пункт меню ) и определить его следующим образом:

    clauses
        onFileNew(_Source, _MenuTag):-
            _ = settingsDialog::display(This).
    

    Для того чтобы окно установки параметров открывалось сразу при запуске приложения, изменим также определение предиката onShow в имплементации класса taskWindow так, как показано ниже:

    clauses
        onShow(_, _CreationData):-
            _MessageForm = messageForm::display(This),
            _ = settingsDialog::display(This).
    

    Создание класса game

    Создадим класс game. В интерфейсе game объявим основные параметры игры. Позднее будет добавлено объявление предикатов.

    constants
        compPlayer : positive = 0.
        humanPlayer : positive = 1.
        winner : positive = 1.
        draw : positive = 0.
    
    domains
        sq = sq(integer Row, integer Column, state).	% поле
        state = darkStone; lightStone; empty.		% состояние поля
    
    properties
        m : integer.					% число строк
        n : integer. 					% число столбцов
        isComputerFirst : boolean.		% комп. ходит первым 
        isDarkStone : boolean. 		% польз. играет темными
    

    В декларации класса game объявим конструктор и свойство.

    constructors
        new: (integer M, integer N, boolean CompFirst, boolean DarkSt).
    
    properties
        game : game.
    

    Ниже приведено определение объявленных свойств и конструкторов в имплементации класса game.

    facts
        m : integer := 15.
        n: integer := 15.
        isComputerFirst : boolean.
        isDarkStone : boolean.
    
    clauses
        new(M, N, IsComputerFirst, IsDarkStone):-
            m := M,
            n := N,
            isComputerFirst := IsComputerFirst,
            isDarkStone := IsDarkStone.
    
    class facts
        game : game := erroneous.
    

    Создание игрового окна

    С помощью диалогового окна Create Project Item создадим поле для игры (Control) и назовем его gameControl. Закроем редактор окна gameControl.

    Далее создадим форму ().

    (рис 9.10) Форма для игры

    Затем с помощью диалогового окна Create Project Item создадим клетку (Draw Control) под названием cellControl. Добавим в интерфейс cellControl объявление свойств и предиката setCellState.

    properties
        gameCtl : gameControl.
        i : integer.
        j : integer.
        state : game::state.
    
    predicates
        setCellState : (game::state).
    

    В имплементацию класса cellControl добавим код, приведенный ниже.

    constants
        bgColor : color = color_BlanchedAlmond.		    % цвет фона
        lineColor : color = color_BurlyWood. 		      % цвет линий
        darkBrushColor : color = color_Sienna.  		  % цвет камня
        darkBorderColor : color = color_SaddleBrown. 	% граница камня
        lightBrushColor : color = color_LightGray.
        lightBorderColor : color =  color_DarkGray.
    
    facts
        gameCtl : gameControl := erroneous.
        i : integer := 0.
        j : integer := 0.
        state : state := empty.
    
    clauses
        setCellState(State):-
            state := State,
            invalidate().
    

    Далее добавим в интерфейс gameControl объявление свойств, хранящих указатели на объекты классов game и gameForm.

    properties
        game : game.
        gameFrm : gameForm.
    

    Изменим раздел open имплементации класса gameControl так, как показано ниже:

    open core, vpiDomains, cellControl, game
    

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

    facts
        gameFrm : gameForm := erroneous.
        game : game := erroneous.
    
    facts
        cell: (integer Row, integer Column, cellControl).
    
    predicates
        createControls: ().
    clauses
        createControls():-
            foreach I = std::cIterate(game:m), J = std::cIterate(game:n)
            do
                Cell = cellControl::new(This),
                Cell:i := I,
                Cell:j := J,
                Cell:gameCtl := This,
                Cell:show(),
                assert(cell(I, J, Cell))
            end foreach.
    
    predicates
        setControls: ().
    clauses
        setControls():-
            getSize(Width, Height),
            Wc = Width div game:n,
            Hc = Height div game:m,
            foreach cell(I, J, Cell) do
                Cell:setSize(Wc, Hc),
                Cell:setPosition(Wc * J, Hc * I),
                Cell:invalidate()
            end foreach.
    

    В редакторе окна gameControl добавим обработчики событий ShowListener и SizeListener. Определение предикатов приведено ниже.

    clauses
        onShow(_Source, _Data):-
            createControls(),
            setControls().
    
    clauses
        onSize(_Source):-
            setControls().
    

    При изменении размеров окна пропорциональным образом изменяется размер клеток и камней.

    В восточных играх камни часто ставятся на пересечение линий сетки.

    В редакторе окна cellControl добавим обработчик событий PaintResponder. Определение предиката onPaint приведено ниже. Сначала закрашивается фон клетки, затем проводятся фрагменты линий сетки — горизонтальный отрезок и вертикальный отрезок. Отрезки проходят через центр клетки. Правила вычисления координат концов отрезков определяются тем, лежит ли клетка на границе игрового поля.

    clauses
        onPaint(_Source, Rectangle, GDI):-
            rct(L, T, R, B) = Rectangle,
            % фон
            GDI:setPen(pen(1, ps_Solid, bgColor)), 
            GDI:setBrush(brush(pat_Solid, bgColor)),
            GDI:drawRect(Rectangle),
            % координаты центра клетки
            X = (L + R) div 2,
            Y = (T + B) div 2,
            % концы горизонтального отрезка
            HorPnt1 = pnt(endPntCoord(j, 0, X, L), Y),
            HorPnt2 = pnt(endPntCoord(j, gameCtl:game:n - 1, X, R), Y),
            % концы вертикального отрезка
            VertPnt1 = pnt(X, endPntCoord(i, 0, Y, T)),
            VertPnt2 = pnt(X, endPntCoord(i, gameCtl:game:m - 1, Y, B)),
            % линии сетки
            GDI:setPen(pen(1, ps_Solid, lineColor)),
            GDI:drawLine(HorPnt1, HorPnt2),
            GDI:drawLine(VertPnt1, VertPnt2),
            % камень
            if state <> empty then
                Dx = (R - L) div 8, Dy = (B - T) div 8,    % размеры отступа
                stoneColor(state, BorderColor, BrushColor),
                GDI:setPen(pen(3, ps_Solid, BorderColor)),
                GDI:setBrush(brush(pat_Solid, BrushColor)),
                GDI:drawEllipse(rct(L + Dx, T + Dy, R - Dx, B - Dy))
            end if.
    
    predicates
        stoneColor: (state, color Border [out], color Brush [out]).
        endPntCoord: (integer J, integer J, integer X, integer L) -> integer.
    clauses
        stoneColor(darkStone, darkBorderColor, darkBrushColor):- !.
        stoneColor(_, lightBorderColor, lightBrushColor).
     
        endPntCoord(J, J, X, _) = X:- !.
        endPntCoord(_, _, _, L) = L.
    

    В интерфейсе gameForm объявим свойство для хранения имени игрока-пользователя.

    properties
        name : string.
    

    Изменим раздел open имплементации класса gameForm следующим образом:

    open core, vpiDomains, game
    

    Далее следует изменить определение конструктора new/1 и определить объявленное свойство.

    clauses
        new(Parent):-
            formWindow::new(Parent),
            generatedInitialize(),
            %
            gameControl_ctl:gameFrm := This,
            gameControl_ctl:game := game::game.
    
    facts
        name : string := "игрок".
    

    Теперь, наконец, мы сможем открыть форму. Форма gameForm открывается при нажатии на кнопку Ok окна settingsDialog. Откроем редактор окна settingsDialog, добавим обработчик события нажатия на эту кнопку и определим его так, как показано ниже.

    clauses
        onOkClick(_Source) = button::defaultAction:-
            Name = string::trim(name_ctl:getText()),
            IsDarkStone = toBoolean(
                radioButton::checked = dark_ctl:getRadioState()),
            Game = game::new(
                row_ctl:getInteger(),
                column_ctl:getInteger(),
                checkBox_ctl:getChecked(),
                IsDarkStone),
            game::game := Game,
            Form = gameForm::new(getParent()),
            if Name <> "" then Form:name := Name end if,
            Form:show().
    

    После нажатия на клавишу Ok закрывается окно settingsDialog и открывается окно gameForm.

    Реализация игры

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

    Реализация выбора хода

    В интерфейсе game объявим предикаты gameOver/2 и move/3. Предикат gameOver проверяет, выполнено ли условие окончания игры. Предикат move возвращает индексы поля, в которое игрок-компьютер ставит свой камень.

    predicates
        gameOver: (sq* Position, state Stone) -> positive Result determ.
    
    predicates
        move: (sq* Position, state CompStone, state HumanStone) 
            -> tuple{integer Row, integer Column}.
    

    В декларации класса game объявим предикаты insert/3 и isCorrect/3. Первый предикат добавляет поле, в которое сделан ход, в текущую позицию и возвращает новую позицию. Второй предикат проверяет, является ли ход в указанное поле допустимым.

    predicates
        insert: (tuple{integer, integer}, state Stone, sq* Position) -> sq*.
    
    predicates
        isCorrect: (integer Row, integer Column, sq* Position) determ.
    

    Ниже приведено определение объявленных предикатов в имплементации класса game.

    Список полей, на которых стоят камни, упорядочивается (см. определение предиката insert). Поле допустимо в данной позиции, если разница между индексами, как по строкам, так и по столбцам, не превышает двух (см. определение предиката isCorrect).

    % вставка поля в позицию
    clauses
        insert(tuple(I, J), Stone, Pos) = list::sort([sq(I, J, Stone) | Pos]).
    
    % проверка допустимости поля
    clauses
        isCorrect(I, J, Position):-
            maxdist(I, J, Position, 2).
    
    class predicates
        maxdist: (integer I, integer J, sq*, integer R) determ.
        maxdist: (tuple{integer, integer}, tuple{integer, integer}*, 
            integer R) determ.
        dist: (tuple{integer, integer}, tuple{integer, integer}, integer)
            determ.
        dist: (integer, integer) -> integer.
    clauses
        maxdist(I, J, L, R):-
            list::all(L, {(sq(I1, J1, _)):- dist(tuple(I, J), tuple(I1, J1), R)}).
    
        maxdist(Sq, ML, R):-
            list::all(ML, {(Sq1):- dist(Sq, Sq1, R)}).
    
        dist(tuple(I1, J1), tuple(I2, J2), R):-
            math::max(dist(I1, I2), dist(J1, J2)) <= R.
    
        dist(I1, I2) = math::abs(I1 - I2).
    

    Предикат all/2 истинен, если указанное условие выполняется для каждого элемента списка. Предикат maxdist истинен, если расстояние от заданного поля до остальных полей не превосходит R. Расстояние определяется как максимальная абсолютная величина разности между номерами строк и столбцов.

    Игра закончена, если в текущей позиции имеется три камня одного цвета в ряд, либо на доске стоят девять камней (см. определение предиката gameOver).

    clauses
        gameOver(Position, S) = winner:-		% три камня S в ряд
            existsFullRow(Position, S),
            !.
        gameOver(Position, _) = draw:-		% ничья
            9 = list::length(Position).
    
    class predicates
        existsFullRow: (sq* Position, state Stone) determ.
        subset: (positive, sq*, state Stone) -> sq* nondeterm.
        isFullRow: (sq*) determ.
    clauses
        existsFullRow(Position, S):-
            Row = subset(3, Position, S),
            isFullRow(Row),
            !.
    
        subset(0, _, _) = []:- !.
        subset(N, [sq(I, J, S) | L], S) = [sq(I, J, S) | subset(N - 1, L, S)].
        subset(N, [_ | L], S) = subset(N, L, S).
    
        isFullRow([sq(I, J, _), sq(I, J + 1, _), sq(I, J + 2, _)]):- !.
        isFullRow([sq(I, J, _), sq(I + 1, J, _), sq(I + 2, J, _)]):- !.
        isFullRow([sq(I, J, _), sq(I + 1, J + 1, _), sq(I + 2, J + 2, _)]):- !.
        isFullRow([sq(I, J, _), sq(I + 1, J - 1, _), sq(I + 2, J - 2, _)]).
    

    Первым аргументом предиката move/3 является текущая позиция — список полей, в которых стоят камни, вторым аргументом — цвет камня компьютера и третьим — цвет камня его противника. Последним аргументом предиката move/4 является список допустимых полей, т. е. полей, в которые игрок-компьютер может сделать ход.

    clauses
        move(Pos, S, S1) = move(Pos, S, S1, getMoves(Pos)).
    
    predicates
        getMoves: (sq*) -> tuple{integer, integer}*.
    clauses
        getMoves(Position) = getMoves(Position, ML):-
            [sq(I, J, _) | _] = Position,
            !,
            ML = [tuple(I1, J1) ||
                I1 = std::fromTo(math::max(0, I-2), math::min(I+2, m-1)),
                J1 = std::fromTo(math::max(0, J-2), math::min(J+2, n-1)),
                not((I1 = I, J1 = J))].
        getMoves(_) = [].
    
    predicates
        getMoves: (sq*, tuple{integer, integer}*) 
            -> tuple{integer, integer}* MoveList.
    clauses
        getMoves(Position, ML) = list::filter(ML, {(tuple(I, J)):-
            not(sq(I, J, _) in Position), isCorrect(I, J, Position)}).
    

    Ниже приводится реализация правил хода игрока-компьютера.

    predicates
        move: (sq*, state, state, tuple{integer, integer}* MoveList) 
            -> tuple{integer, integer} Move.
    clauses
        % последний ход
        move(_, _, _, [Move]) = Move:- !.
        % ход по диагонали от первого камня первого игрока
         move([Sq], _, _, ML) = Move:-
            MoveList = [M || M = diagMove(Sq, ML)],
            Move = randomMove(MoveList),
            !.
        % три в ряд - выигрыш
        move(Position, S, _, ML) = Move:-
            Move = block(ML, Position, S),
            !.
       % блок, не приводящий к вилке или прямому проигрышу
         move(Position, S, S1, ML) = Move:-
            Move = block(ML, Position, S1),
            nextPosition(ML, Position, S, Move, Position2, ML2),
            not((nextPosition(ML2, Position2, S1, _, Position3, ML3),
                isFork(ML3, Position3, S1))),
            not(_ = block(ML2, Position2, S1)),
            !.
        % блок, если поле определено
        move(Position, _S, S1, ML) = Move:-
            isGamePlaceDefined(Position, ML),
            Move = block(ML, Position, S1),
            !.
        % вилка
        move(Position, S, _S1, ML) = Move:-
            nextPosition(ML, Position, S, Move, Position1, ML1),
            isFork(ML1, Position1, S),
            !.
        % вынуждаем ставить блок, чтобы поставить вилку
        move(Position, S, S1, ML) = Move:-
            nextPosition(ML, Position, S, Move, Position1, ML1), %ход комп
            Move1 = block(ML1, Position1, S), 		% ход человека
            not((nextPosition(ML1, Position1, S1, Move2, Pos0, ML0),
                Move2 <> Move1,
                not(_ = block(ML0, Pos0, S)))),
            nextPosition(ML1, Position1, S1, Move1, Position2, ML2),
            nextPosition(ML2, Position2, S, _, Position3, ML3), % ход комп
            isFork(ML3, Position3, S),
            not(_ = block(ML3, Position3, S1)),
            !.
        % ход в центр, если место определено
        move(Position, _, _, ML) = Move:-
            Move = centerMove(Position, ML),
            !.
        % предотвращение возможной вилки
        move(Position, S, S1, ML) = Move:-
            nextPosition(ML, Position, S, Move, Position1, ML1),
            isGamePlaceDefined(Position1, ML1),
            not((nextPosition(ML1, Position1, S1, _, Position2, ML2),
                not(_ = block(ML2, Position2, S)),
                isFork(ML2, Position2, S1))),
            not(_ = centerMove(Position1, ML1)),
            !.
        % не даем ставить вилку – не исп.
        move(Position, _S, S1, ML) = Move:-
            nextPosition(ML, Position, S1, Move, Position1, ML1),
            isFork(ML1, Position1, S1),
            !.
        % случайный ход – не исп.
        move(_Position, _S, _S1, ML) = randomMove(ML).
    

    Ниже определяются вспомогательные предикаты

    predicates 			% выбор хода и определение позиции
        nextPosition: (tuple{integer, integer}*, sq*, state, 
            tuple{integer, integer}, sq*) 
                nondeterm (i,i,i,o,o) determ (i,i,i,i,o).
    clauses
        nextPosition(ML, Pos, S, Move, insert(Move, S, Pos)):-
            Move in ML.
    
    predicates 		% выбор хода, позиции и допустимых ходов 
        nextPosition: (tuple{integer, integer}*, sq*, state, 
            tuple{integer, integer}, sq*,  tuple{integer, integer}*)
                nondeterm (i,i,i,o,o,o) determ (i,i,i,i,o,o).
    clauses
        nextPosition(ML, Pos, S, Move, Pos1, getMoves(Pos1, ML)):-
            nextPosition(ML, Pos, S, Move, Pos1).
    
    predicates 							% блок
        block: (tuple{integer, integer}*, sq*, state) 
            -> tuple{integer, integer} nondeterm.
    clauses
        block(ML, Position, S) = Move:- 
            nextPosition(ML, Position, S, Move, Position1),
            existsFullRow(Position1, S).
    
    predicates				% проверка, поставлена ли вилка
        isFork: (tuple{integer, integer}*, sq*, state) determ.
    clauses
        isFork(ML, Position, S):- 
            isGamePlaceDefined(Position, ML),
            Move1 = block(ML, Position, S),
            Move2 = block(ML, Position, S),
            Move1 <> Move2,
            !.
    
    predicates				% определен ли участок игры
        isGamePlaceDefined: (sq*, tuple{integer, integer}*) determ.
    clauses
        isGamePlaceDefined(Position, ML):- 
            9 = list::length(Position) + list::length(ML).
    
    predicates						% ход в центр
        centerMove: (sq*, tuple{integer, integer}*) 
            -> tuple{integer, integer} determ.
    clauses
        centerMove(Position, ML) = tuple(I, J):- 	
            isGamePlaceDefined(Position, ML),
            tuple(I, J) in ML,
            maxdist(I, J, Position, 1),
            maxdist(tuple(I, J), ML, 1),
            !.
    
    predicates						% ход по диагонали
        diagMove: (sq, tuple{integer, integer}*) 
            -> tuple{integer, integer} nondeterm.
    clauses
        diagMove(sq(I, J, _), ML) = tuple(I1, J1):- 
            tuple(I1, J1) in ML,
            1 = dist(I, I1),
            1 = dist(J, J1).
    
    class predicates 			 		% случайный ход
        randomMove: (tuple{integer, integer}*) 
            -> tuple{integer, integer}.
    clauses
        randomMove(ML) = 
            list::nth(math::random(list::length(ML)), ML).
    

    Сообщения о ходах игроков

    Сообщения о ходе игрока и о результате игры выводятся в строке заголовка окна gameForm. Объявим соответствующие предикаты в интерфейсе gameForm.

    predicates
        moveMessage: (positive Player).
        gameOverMessage: (positive Result, positive Player).
    

    Ниже приведено определение этих предикатов в имплементации класса gameForm.

    clauses
        moveMessage(Player):-
            setText(string::format("Ходит %", playerName(Player))).
    
    clauses
        gameOverMessage(winner, Player):- !,
            setText(
                string::format("Победитель: %!", playerName(Player))).
        gameOverMessage(_Result, _Player):-
            setText("Ничья!").
    
    predicates
        playerName : (positive Player) -> string Name.
    clauses
        playerName(humanPlayer) = name:- !.
        playerName(_) = "компьютер".
    

    Реализация хода игры

    Ходом игры управляет класс gameContol. Объявим в интерфейсе gameControl необходимые свойства и предикаты. Предикат startGame обрабатывает начало игры. Предикат isCorrect/2 проверяет, является ли допустимым ход пользователя. Предикат humanMove/2 обрабатывает ход пользователя.

    properties
        isGameOver : boolean.
    
    predicates
        startGame: ().
        isCorrect: (cellControl) determ.
        humanMove: (cellControl).
    

    Определение свойств и предикатов в имплементации класса gameControl приведено ниже.

    facts
        isGameOver : boolean := false.
        humanStone : state := darkStone.
        compStone : state := lightStone.
        currentPosition : sq* := []. 
    
    clauses
        startGame():-
            false = game:isDarkStone,
            humanStone := lightStone,
            compStone := darkStone,
            fail.
        startGame():-
            true = game:isComputerFirst,
            !,
            gameFrm:moveMessage(compPlayer),
            Move = firstMove(),
            showMove(Move).
        startGame():-
            gameFrm:moveMessage(humanPlayer).
    
    clauses
        isCorrect(Cell):-
            isCorrect(Cell:i, Cell:j, currentPosition).
    
    clauses
        humanMove(Cell):-			% обработка хода пользователя
            Cell:setCellState(humanStone),
            updatePosition(tuple(Cell:i, Cell:j), humanStone),
            move(compPlayer).		% переход хода к компьютеру
    
    predicates				% первый ход в случайное поле
        firstMove: () -> tuple{integer, integer}.
    clauses
        firstMove() = tuple(F(game:m), F(game:n)):-
            F = {(N) = math::random(N - 2 * D) + D:-
                	D = if N > 4 then 2 else 1 end if}.
    
    predicates
        updatePosition: (tuple{integer, integer}, state Stone).
    clauses
        updatePosition(Move, Stone):-
            currentPosition := insert(Move, Stone, currentPosition).
    
    predicates
        move: (positive Player).
        makeMove: (positive Player).
    clauses
        move(Player):-
            gameOver(currentPosition, nextPlayer(Player)),
            !.
        move(Player):-
            gameFrm:moveMessage(Player),
            makeMove(Player).
    
        makeMove(compPlayer):- !,
            Move = game:move(currentPosition, compStone, humanStone),
            showMove(Move).
        makeMove(_).
    
    predicates
        gameOver: (sq*, positive) determ.
    clauses
        gameOver(Position, Player):-
            Result = game:gameOver(Position, stone(Player)),
            isGameOver := true,
            gameFrm:gameOverMessage(Result, Player).
    
    predicates
        stone: (positive Player) -> state.
        nextPlayer: (positive Player) -> positive NextPlayer.
    clauses
        stone(compPlayer) = compStone:- !.
        stone(_) = humanStone.
    
        nextPlayer(compPlayer) = humanPlayer:- !.
        nextPlayer(_) = compPlayer.
    
    predicates
        showMove: (tuple{integer, integer}).
     clauses
        showMove(Move):-
            getCell(Move):setCellState(compStone),
            updatePosition(Move, compStone),
            move(humanPlayer).		% переход хода к пользователю
    
    predicates
        getCell: (tuple{integer, integer}) -> cellControl.
    clauses
        getCell(tuple(I, J)) = Cell:-
            cell(I, J, Cell),
            !.
        getCell(_) = _:-
            exception::raise_error().
    

    Пользователь ходит с помощью мыши. В редакторе окна cellControl добавим обработчик событий MouseDownListener. Ниже приведено его определение.

    clauses
        onMouseDown(_Source, _Point, _ShiftControlAlt, _Button):-
            empty = state,
            false = gameCtl:isGameOver,
            gameCtl:isCorrect(This),
            !,
            gameCtl:humanMove(This).
        onMouseDown(_Source, _Point, _ShiftControlAlt, _Button).
    

    Остается открыть редактор формы gameForm и добавить обработчик событий ShowListener. Ниже приведено его определение.

    clauses
        onShow(_Source, _Data):-
            gameControl_ctl:startGame().
    

    Упражнения

    9.1. Напишите реализацию игры "Четыре в ряд", используя для хода игрока-компьютера правила и оценочную функцию.

    9.2. Напишите реализацию игры "Пять в ряд", используя для хода игрока-компьютера правила и оценочную функцию.

    9.3. Реализуйте обычную игру "Крестики-нолики" 3 х 3, в которой игрок-компьютер играет на разных уровнях сложности.

    9.4. Напишите самообучающуюся программу игры в "Крестики-нолики".

    Вернуться к учебному плану