Система программирования Turbo Pascal

           

Вид экрана в начале игры ним



Вид экрана в начале игры ним

Procedure Prepare;

{Подготовка данных и экрана к игре}

const

Header0 = 'ИГРА НИМ';

Headerl = 'Вы можете взять любое число фишек из любого ряда.';

Header2 = 'Выигрывает тот, кто возьмет последнюю фишку.';

Headers = 'Номер ряда';

Header4 = 'Количество фишек';

var

i : Integer; begin {Prepare}

ClrScr; {Очищаем экран}

{Выводим строки заголовка:}

GotoXY((80-Length(Header0)) div 2,1);

Write(HeaderO) ;

GotoXY((80-Length(Headerl)) div 2,2);



Write(Headerl);

GotoXY((80-Length(Header2)) div2,3); 

Writeln(Header2);

Write(Header3); 

GotoXY(80-Length(Header4),4); 

Write(Header4); 

{Готовим начальную раскладку:} 

for i := 1 to nrow do

col [i] := ncol[i] 

end; {Prepare}

Для вывода верхних строк строго посередине экрана используется задание горизонтальной координаты курсора для процедуры GotoXY как половины от разницы между полной длиной экрана (80 позиций) и длиной выводимой строки (определяется с помощью функции LENGTH).

В процедуре GetPlayerMove осуществляются ввод, контроль и отображение на экране очередного хода игрока. Предварительно нужно показать игроку текущее состояние игрового поля. Поскольку поле будет обновляться как минимум дважды (после хода игрока и после хода программы), действия, связанные с изображением поля на экране, следует вынести в отдельную процедуру. Назовем ее ShowField и займемся ее реализацией.

Судя по всему, нам понадобится организовать цикл; в ходе цикла для каждого ряда игрового поля будет выведена строка, в левой части которой указывается номер ряда, в правой - текущее количество фишек в нем, а посередине выводятся символы, имитирующие фишки. В принципе, можно выбрать любой символ ПК для обозначения фишки, например, X или О. Я предпочел воспользоваться символом псевдографики с кодом 220: этот символ представляет собой небольшой квадратик и легко ассоциируется с фишкой.

Procedure ShowField;

{ Отображает на экране текущее состояние игрового поля }

const

FISH = #220; {Символ-указатель фишки} 

Х0 = 4; {Левая колонка номеров рядов} 

X1 =72; {Правая колонка количества фишек} 

X = 20; {Левый край игрового поля} 

var

i,j : Integer; 

begin {ShowField} 

for i := 1 to nrow do begin

GotoXY(X0,i+4);

Write(i); {Номер ряда}

GotoXY(X1,i+4);

Write(col[i]:2); {Количество фишек в ряду}

for j := 1 to ncol[i] do {Вывод ряда фишек:}

begin

GotoXY(X+2*j,i+4); if j[i] then

Write(FISH) 

else

Write('.') 

end 

end 

end; {ShowField}

Символы FISH (квадратики) выводятся через одну позицию, чтобы не сливались на экране. В те позиции, в которых ранее стояли уже снятые с поля фишки, выводится точка.

Теперь вернемся к процедуре GETPLAYERMOVE. При вводе любого очередного хода игрок должен задать два целых числа X1 и Х2. Первое из них указывает номер ряда, а второе - количество фишек, которые игрок хочет забрать из этого ряда. Программа должна проконтролировать правильность задания этих чисел: X1 должно указывать непустой ряд, Х2 не может превышать количество фишек в этом ряду. Кроме того, мы должны условиться о двух особых случаях:

  • пользователь больше не хочет играть и дает команду завершить работу программы;
  • пользователь хочет изменить условия игры.

Пусть ввод числа X1 =0 означает команду выхода из программы, а X1 = -1 - команду изменения условий игры. Тогда можно написать такой начальный вариант процедуры:

Procedure GetPlayerMove;

{Получает, контролирует и отображает ход игрока}

var

correctly : Boolean; {Признак правильности сделанного хода} 

xl,x2 : Integer; {Вводимый ход} 

begin {GetPlayerMove}

{Показываем начальное состояние игрового поля}

ShowField;

{Сообщаем, игроку правила ввода хода}

repeat

{Приглашаем игрока ввести ход}

ReadLn(xl,x2); {Вводим очередной ход} 

exit := xl=0; {Контроль команды выхода} 

change := xl=-l; {Контроль команды изменения} 

if not (exit or change) then

{Проверить правильность хода и установить нужное значение переменной CORRECTLY. Если ход правильный, сделать нужные изменения в раскладке фишек и показать поле.}  

else

correctly := true {Случай EXIT или CHANGE} 

until correctly; if change then

{ Изменить условия игры }  

end; {GetPlayerMove}

В этом варианте в процедуре GetPlayerMove нет описания процедуры SHOWFIELD. Сделано это не случайно: процедура ShowField может понадобиться также и при реализации процедуры SetOwnerMove, поэтому она должна быть глобальной по отношению и к GetPlayerMove, и к SetOwnerMove, т.е. ее описание должно в тексте программы предшествовать описаниям двух использующих ее процедур.

Действия

{ Сообщить игроку правила ввода хода } ,

{ Пригласить игрока ввести ход } 

и

{Проверить правильность хода и установить нужное значение переменной Correctly. Если ход правильный, сделать нужные изменения в раскладке фишек и показать поле.}

не очень сложны в реализации, поэтому их можно осуществить непосредственно в теле процедуры GETPLAYERMOVE. Иное дело - изменение условий игры. Это действие полезно реализовать в отдельной процедуре GETCHANGE. С учетом этого второй вариант процедуры GETPLAYERMOVE примет такой вид:

Procedure GetPlayerMove;

{Получает, контролирует и отображает ход игрока}  

const

ТЕХТ1 = 'Введите Ваш ход в формате РЯД КОЛИЧ ';

ТЕХТ01= ' (например, 2 3- взять из 2 ряда 3 фишки) ' ;

ТЕХТ2 = 'или введите 0 0 для выхода из игры; ' ; . 

ТЕХТ02= '-1 0 для настройки игры';

ТЕХТЗ = 'Ваш ход:                            ';

Y = 20; {Номер строки для вывода сообщений} 

var

correctly : Boolean; {Признак правильности сделанного хода}

xl,x2 : Integer; {Вводимый ход}

{-----------------}

Procedure GetChange;

{Устанавливает новую настройку игры (количество рядов и количество фишек в каждом ряду}  

begin {GetChange} 

end; {GetChange}

{-----------------}

begin {GetPlayerMove}

ShowField; {Показываем начальное состояние поля}

{Сообщить игроку правила ввода хода:}

GotoXY((80-Length(TEXT1+TEXT01)) div2,Y);

Write(TEXT1+TEXT01);

GotoXY((80-Length(TEXT2+TEXT02)) div2,Y+l);

Write(TEXT2+TEXT02);

repeat

{Пригласить игрока ввести ход:}

GotoXY(l,Y+2);

Write(TEXTS); {Выводим приглашение и стираем предыдущий ход}

GotoXY(WhereX-16,Y+2); {Курсор влево на 16 позиций}

ReadLn(xl,x2); {Вводим очередной ход} 

exit := xl=0; {Контроль команды выхода} 

change := xl=-l; {Контроль команды изменения}

 if not (exit or change) then 

begin

correctly := (xl > 0) and (xl <= nrow) and (x2 <= col[xl]) and (x2 > 0) ; 

if correctly then

begin {Ход правильный:}

col[xl] := col[xl]-x2; {Изменяем раскладку фишек} 

ShowField {Показываем поле} 

end 

else

Write(#7) {Ход неправильный: дать звуковой сигнал} 

end 

else

correctly := true {Случай EXIT или CHANGE} 

until correctly; 

if change then

GetChange end; {GetPlayerMove}

Обратите внимание: константа

ТЕХТЗ = 'Ваш ход:

имеет длинный «хвост» из пробелов (их 17), поэтому после вывода этого приглашения курсор возвращается влево на 16 позиций оператором

GotoXY(WhereX-16,Y+2); {курсор влево на 16 позиций}

(функция WHEREX возвращает текущую горизонтальную координату курсора, а функция WHEREY - его вертикальную координату). Сделано это для того, чтобы в случае, если игрок ввел неверный ход и программа повторяет вывод приглашения, пробелы в константе ТЕХТЗ затерли бы строку предыдущего ввода.

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

Procedure GetChange;

{Устанавливает новую настройку игры (количество рядов и количество фишек в каждом ряду} 

const

tl='HACTPOЙKA ИГРЫ';

t2 ='(ввод количества рядов и количества '+'фишек в каждом ряду)'; 

var

correctly : Boolean;

i : Integer; begin {GetChange}

ClrScr;

GotoXY( (80 -Length (tl) ) div2,l);

Write(tl) ;

GotoXY( (80 -Length (t2) ) div2,2);

Write (t2);

repeat

GotoXYd, 3) ;

Write ( 'Введите количество рядов (максимум ', MAXROW, '):'); 

GotoXY(WhereX-6,WhereY) ; 

ReadLn (nrow) ;

correctly := (nrow <= MAXROW) and (nrow > 1) ; 

if not correctly then

Write (#7) 

until correctly; 

for i := 1 to nrow do 

repeat

GotoXY(l,i+3) ;

Write (' ряд ',i,', количество фишек (максимум ', MAXCOL , ' ) : ' ) ; 

GotoXY (WhereX - 6 , WhereY) ; 

ReadLn (ncol [i] ) ;

correctly := (ncol [i] <= MAXCOL) and (ncol[i] > 0) ; 

if not correctly then

Write (#7) 

until correctly 

end; {GetChange}

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

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

Procedure SetOwnerMove;

{Находит и отображает очередной ход программы}

{-----------------}

Function CheckField : Integer;

{Проверяет состояния игры. Возвращает 0, если нет ни одной фишки (победа игрока) , 1 - есть один ряд (победа машины) и - количество непустых рядов в остальных случаях}

begin {CheckField} 

end; {CheckField}

{-----------------}

Procedure PlayerVictory;

{Поздравить игрока с победой и усложнить игру}

begin {PlayerVictory} 

end; {PlayerVictory}

{-----------------}

Procedure OwnVictory; 

{Победа машины} 

begin {OwnVictory} 

end; {OwnVictory}

{-----------------}

Procedure ChooseMove;

{Выбор очередного хода}  

begin {ChooseMove} 

end; {ChooseMove}

{-----------------}

begin {SetOwnerMove} 

case CheckField of {Проверяем количество непустых рядов} 

0 : PlayerVictory; {Все ряды пусты - победа игрока} 

1 : OwnVictory; {Один непустой ряд - победа машины} 

else

ChooseMove; {Выбираем очередной ход} 

end; {case}

end; {SetOwnerMove}

Функция CHECKFIELD и процедуры PLAYERVICTORY и OWNVICTORY достаточно просты и их текст помещается без каких-либо пояснений в окончательный вариант программы (см. прил.5.3). Отмечу лишь, что в случае победы игрока нет смысла повторять партию заново с той же самой раскладкой фишек. Поэтому игра усложняется: в исходную раскладку добавляется еще по одной фишке в каждый ряд.

В процедуре CHOOSEMOVE анализируется позиция и выбирается очередной ход программы. Описание оптимальной стратегии уже приводилось выше. Действия программы заключаются в поиске первого слева (старшего) двоичного разряда, для которого сумма чисел нечетная. Если такой разряд не обнаружен, то текущая позиция безопасна для игрока, а значит любой ход программы сделает ее опасной. В этом случае для программы не существует оптимального выбора и она лишь убирает одну фишку из любого непустого ряда. Такая тактика означает пассивное ожидание ошибки игрока.

Если обнаружен разряд i с нечетной суммой, программа приступает к реализации оптимальной стратегии и тогда игрок обречен на поражение. Для выбора ряда, из которого следует взять фишки, программа просматривает последовательно все ряды и отыскивает тот ряд j, количество фишек в котором (в двоичном представлении) дает единицу в разряде i. Значение этого разряда для количества фишек в ряду j заменяется нулем. Затем программа продолжает подсчет суммы для оставшихся младших разрядов. Если в каком-либо из них вновь обнаружена нечетность, значение этого разряда для количества фишек в рядуj инвертируется, т.е. 0 заменяется на 1, а 1 на 0. Например, если двоичные представления числа фишек и четности сумм таковы:

число фишек в ряду j: 01001 

четность сумм: 01011

(единицей указаны разряды с нечетными суммами), то в результате этой операции получим:

число фишек в ряду j: 00010 

четность сумм: 00000

Таким образом, в исходном состоянии в ряду j было 1001 =9 фишек, безопасная позиция требует, чтобы в ряду осталось 0010 = 2 фишки, следовательно, из него нужно забрать 9-2 = 7 фишек.

Окончательный вариант программы представлен в прил.5.3. Попробуйте разобраться в ее деталях самостоятельно.

В программной реализации алгоритма широко используется то обстоятельство, что Ваш компьютер, как и все остальные вычислительные машины, работает с числами, представленными в двоичной системе счисления. Поэтому для получения двоичного представления числа в процедуре BITFORM оно проверяется на четность с помощью стандартной функции ODD, затем сдвигается вправо на один двоичный разряд (операция SHR), вновь осуществляется проверка на четность и т.д. до тех пор, пока не будут проверены все разряды. Максимальное число двоичных разрядов, достаточное для двоичного представления количества фишек в ряду MAXCOL=63, задается константой ВIТ=6.

Для получения суммы двоичных разрядов в процедуре CHOOSEMOVE используется суммирование разрядов по модулю 2 с помощью операции XOR. Такое суммирование дает 0, если количество единиц четное или равно нулю, и 1 - если нечетное. В этой же процедуре для инверсии двоичного разряда применяется оператор

if nbit[i] = 1 then

ncbit[j,i] := ord(ncbit[j,i]=0); {Инверсия разрядов},

в котором используется соглашение о внутреннем представлении логических величин в Турбо Паскале: 0 соответствует FALSE, а 1 - TRUE.



Содержание раздела