Вид экрана в начале игры ним
Вид экрана в начале игры ним
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.