Опубликован: 08.08.2015 | Доступ: свободный | Студентов: 300 / 41 | Длительность: 09:22:00
Лекция 9:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Принуждение к блоку с последующей вилкой. Позиция 1

Рис. 9.3. Принуждение к блоку с последующей вилкой. Позиция 1
Принуждение к блоку с последующей вилкой. Позиция 2

Рис. 9.4. Принуждение к блоку с последующей вилкой. Позиция 2
Принуждение к блоку с последующей вилкой. Позиция 3

Рис. 9.5. Принуждение к блоку с последующей вилкой. Позиция 3
Принуждение к блоку с последующей вилкой. Позиция 4

Рис. 9.6. Принуждение к блоку с последующей вилкой. Позиция 4

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

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

Ход в центральное поле

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

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

На рис. 9.8 (a) рис. 9.8 приведен пример позиции, в которой игрок темными выбирает ход по правилу 8 (рис. 9.8 (b)) рис. 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: светлый.

Теперь следует включить пункт меню File -> New, добавить обработчик событий выбора данной команды меню (см. п. 1.1 "Основные элементы графического интерфейса пользователя" ) и определить его следующим образом:

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. 		% польз. играет темными
Листинг 9.1. Объявление параметров в интерфейсе game

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

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

properties
    game : game.
Листинг 9.2. Объявление свойства в декларации класса 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.
Листинг 9.3. Определение свойств в имплементации класса game

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

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

Далее создадим форму (Form) gameForm. Разместим в поле формы пользовательский элемент управления (CustomControl; Class: gameControl), для всех якорей привязки укажем значение True (рис. 9.10 рис. 9.10).

Форма для игры

Рис. 9.10. Форма для игры

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

properties
    gameCtl : gameControl.
    i : integer.
    j : integer.
    state : game::state.

predicates
    setCellState : (game::state).
Листинг 9.4. Объявление свойств в интерфейсе cellControl

В имплементацию класса 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().
Листинг 9.5. Определение свойств в имплементации класса cellControl

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

properties
    game : game.
    gameFrm : gameForm.
Листинг 9.6. Объявление свойств в интерфейсе gameControl

Изменим раздел 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.
Листинг 9.7. Создание клетчатого поля

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

clauses
    onShow(_Source, _Data):-
        createControls(),
        setControls().
Листинг 9.8. Определение предиката onShow
clauses
    onSize(_Source):-
        setControls().
Листинг 9.9. Определение предиката onSize

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

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

В редакторе окна 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.
Листинг 9.10. Определение предиката onPaint

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

properties
    name : string.
Листинг 9.11. Объявление свойства в интерфейсе gameForm

Изменим раздел 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 := "игрок".
Листинг 9.12. Изменение определения конструктора

Теперь, наконец, мы сможем открыть форму. Форма 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().
Листинг 9.13. Установка параметров игры

После нажатия на клавишу 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}.
Листинг 9.14. Объявление предикатов в интерфейфсе game

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

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

predicates
    isCorrect: (integer Row, integer Column, sq* Position) determ.
Листинг 9.15. Объявление предикатов в декларации класса game

Ниже приведено определение объявленных предикатов в имплементации класса 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).
Листинг 9.16. Обновление позиции. Проверка допустимости хода

Предикат 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, _)]).
Листинг 9.17. Условие окончания игры

Первым аргументом предиката 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)}).
Листинг 9.18. Реализация хода игрока. Вычисление допустимых ходов

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

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).
Листинг 9.19. Правила хода игрока-компьютера

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

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).
Листинг 9.20. Блок, вилка, центральный и диагональный ходы

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

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

predicates
    moveMessage: (positive Player).
    gameOverMessage: (positive Result, positive Player).
Листинг 9.21. Объявление предикатов в интерфейфсе gameForm

Ниже приведено определение этих предикатов в имплементации класса 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(_) = "компьютер".
Листинг 9.21. Определение в имплементации класса gameForm

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

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

properties
    isGameOver : boolean.

predicates
    startGame: ().
    isCorrect: (cellControl) determ.
    humanMove: (cellControl).
Листинг 9.22. Объявление предикатов в интерфейфсе gameControl

Определение свойств и предикатов в имплементации класса 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().
Листинг 9.23. Определение в имплементации класса gameControl

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

clauses
    onMouseDown(_Source, _Point, _ShiftControlAlt, _Button):-
        empty = state,
        false = gameCtl:isGameOver,
        gameCtl:isCorrect(This),
        !,
        gameCtl:humanMove(This).
    onMouseDown(_Source, _Point, _ShiftControlAlt, _Button).
Листинг 9.24. Реализация хода пользователя

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

clauses
    onShow(_Source, _Data):-
        gameControl_ctl:startGame().
Листинг 9.25. Начало игры

Упражнения

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

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

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

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

Алексей Роднин
Алексей Роднин
Россия
Роман Гаранин
Роман Гаранин
Беларусь, Брест