Мы познакомились с основными возможностями языка Турбо Паскаль. Как видите, ядро языка очень компактно и отличается простотой, именно в этом состоит главная заслуга автора Паскаля Н.Вирта: придуманный им, прост и естественен, он легко осваивается, на нем не трудно писать самые разнообразные программы. Конечно, рассмотрены далеко не все свойства Турбо Паскаля, ведь его главная отличительная черта - это богатство типов данных. Однако уже рассмотренного вполне достаточно для написания многих полезных программ.
Приводимые ниже программы относительно сложны, поэтому они реализуются поэтапно, по методу нисходящего программирования, кажется, что тем читателям, кто не имеет большого опыта в программировании или кто захочет подробнее ознакомиться с нисходящим программированием, изучение этой главы принесет определенную пользу. Если будет скучно разбираться в «кухне» программирования, но Вас заинтересуют описываемые здесь программы и Вы захотите их повторить, то в поил 5 Вы найдете полный текст соответствующей программы; однако в каждой из них используются некоторые дополнительные возможности языка Турбо Паскаль, которые не рассматривались ранее и которые обсуждаются в пропущенных Вами фрагментах книги.
При оформлении программ я стремился использовать хороший стиль написания программ, т.е. такую их форму, которая дает наиболее полное представление о структуре программы в целом и ее отдельных частей. Не существует какого-либо стандарта, определяющего хороший стиль программы. Обычно это интуитивное понятие включает способ расположения операторов и описаний по строкам (не рекомендуется размещать более одного оператора на каждой строке), а также выделение отступами тела составных и условных операторов. Последнее особенно важно в программах Турбо Паскаля: сплошь и рядом в них встречаются операторные скобки BEGIN...END, причем часто вложенные друг в друга; использование отступа служит дополнительным средством проверки правильности их расстановки - не случайно в редакторе среды предусмотрена соответствующая опция. Принятый мною стиль оформления программ не претендует на эталон, просто мне кажется, что таким образом оформленные программы читаются лучше. Если Вы всерьез намерены программировать на Турбо Паскале, имеет смысл составить собственное представление о хорошем стиле и далее неукоснительно придерживаться его - очень скоро некоторые дополнительные издержки на подготовку программ с лихвой окупятся их «читабельностью», а это поможет Вам вспомнить все детали реализации программы, которая была написана несколько месяцев тому назад.
12.1. Вычисление дня недели
Случалось ли Вам мучительно вспоминать, какой именно день недели приходился на то или иное число год или два назад, или вычислять, на какой день недели в этом году приходится Ваш день рождения? Если да, то Вас, думаю, заинтересует простая программа, позволяющая по заданной дате мгновенно вычислить соответствующий день недели. В ее основе лежит такая формула:
день недели = остаток от деления Х на 7,
где X=abs(tninc(2.6*m-0.2)+d+y/4+y+ c:/4-2*c);
т - номер месяца (см. ниже);
d - число (день месяца);
с - номер столетия (см. ниже);
у - номер года в столетии.
При использовании этой формулы следует учесть два обстоятельства. Во первых, формула введена для григорианского календаря нового стиля «от 1582 до 4903 года). Во-вторых, год и месяц следует предварительно преобразовать так, как если бы начало года приходилось на 1 марта, иными словами, март в этой формуле имеет порядковый номер 1, апрель ...., январь 11 и февраль 12, причем январь и февраль следует отнести к предыдущему году. Например, для 1 февраля 1991 года номер месяца должен быть равен 12, а год 1990, в то время как для 31 декабря 1991: номер месяца -10, а год -1991. Результат вычисления дается в виде целого числа в диапазоне от О до 6, причем 0 соответствует воскресенью.
Приступим к разработке программы. Прежде всего, предполагая что программа уже создана и Вы осуществляете ее прогон. Какая форма взаимодействия с программой кажется Вам наиболее подходящей? Вряд ли Вас удовлетворит однократное ее исполнение (ввод некоторой дат вывод на экран соответствующего дня недели). Скорее всего Вы захотите повторить работу программы для нескольких дат, например, поинтересоваться, в какой день недели Вы родились, затем, на какой день • приходится в этом году Ваш день рождения, дни рождения друзей; может быть, определить, в какой день родились известные '. исторические деятели, и т.д. Таким образом, в программе следует посмотреть многократное выполнение действий <ввод даты> - <выч дня недели>, причем число циклов вычисления заранее не известно. Сразу же возникает новый вопрос: как сообщить программе, что Вы завершили работу с ней? Для этого можно условиться, что ввод некоторой заранее обусловленной или недопустимой даты должен интерпретироваться программой, как указание на прекращение работы. С учетом сказанного напишем такой начальный вариант программы:
var
correctly : Boolean: (Признак правильной даты)
d.m.y : Integer; (Вводимая дата - день, месяц и год)
BEGIN
repeat
{ Ввести в переменные d, m и у очередную дату и проверить ее. Если дата правильная, установить correctly=true, иначе correctly=false }
If correctly then
{ Вычислить и выдать на экран день недели };
unt11 not correct ly
END.
Если вы попытаетесь запустить эту программу на счет, то ее повторение будет зависеть от начального значения переменной CORRECTLY.' значение случайно, так как компилятор Турбо Паскаля не начальной инициализации переменных. Скорее всего, тот байт оперативной памяти, в котором она разместится, окажется нулевым, что в' Паскале расценивается как логическое значение FALSE, поэтому с большой вероятностью ничего не произойдет, и программа сразу же завершит свою работу (условие NOT CORRECTLY будет выполнено). Если начальное значение CORRECTLY окажется н нулевым, то цикл REPEAT...UNTIL будет выполняться до тех пор, пока Вы не выключите компьютер или не нажмете клавиши Ctrl—Break.
Будем считать, что необходимые действия осуществляются в ; процедурах с именами INPUTDATE (ввод даты) и WRITEDAY (вычисление и печать дня недели). В процедуру INPUTDATE не нужно ничего передавать из программы, так как в ней самой осуществляются контроль даты. Поэтому заголовок процедуры может иметь такой вид:
PROCEDURE lnputDate(var d.m.y : Integer:
var correctly : Boolean);
Процедура WRITEDAY, напротив, только получает из программы нужные ей данные и ничего не возвращает в программу, поэтому в ее заголовке параметры описываются без слова VAR:
PROCEDURE WriteDay(d,m.y : Integer);
С учетом этого программу можно уточнить следующим образом:
var
correctlv : Boolean; {признак правильной даты}
d, m,y : Integer; {вводима» дата - день, месяц и год}
PROCEDURE InputDate(var d.m.y : Integer;
var correctly : Boolean);
{ Ввести в переменные d. m и у очередную дату и проверить ее
ЕСЛИ дата правильная, установить correctly-true, иная
correctly=false }
BEGIN (InputDate)
correctly := false
END: (inputDate)
PROCEDURE WriteDay(d.m,у : Integer):
{ Вычислить и выдать на экран день недели }
BEGIN {WrlteDay}
END; (WriteDay)__________
BEGIN
repeat
InputDate(d,m,y,correctly);
If correctly then
Wri teDay(d,m,y)
until not correct ly
END.
Теперь можно разработать процедуру INPUTDATE. Ввод даты не I вызывает трудностей- стандартные процедуры WRITE и READLN отлично приспособлены для этой цели. Для проверки правильности даты нужно проверить принадлежность месяца диапазону 1...12 и года - диапазону 1582...4903. Кроме того, число не должно выходить из диапазона 1...31. I Если Вы не очень настаиваете на более точной проверке числа в зависимости от месяца и года (для февраля), то программная реализация процедуры будет следующей:
PROCEDURE lnputDate(var d.m.y : Integer);
var correctly : Boolean):
{ ввести в переменные d, m и у очередную дату и проверить
ее. Если дата правильная, установить correctly-true, иначе
correctly=false }
BEGIN {InputDate}
Write(‘Введите дату в формате ДД ММ ГГГГ: ');
readln(d.m.y);
correctly := (d>=1) and (d<=31) and (m>=1)
and (m<-12) and (y>-1582) and
(y<=4903)
End;{InputDate}
При выполнении этой процедуры ввод, например, трех нулей приведет к присвоению переменной CORRECTLY значения FALSE, что вызовет завершение работы программы.
Теперь разберемся с процедурой WRITEDAY. Получив в параметрах обращения день, месяц и год, она должна:
• преобразовать месяц и год так, как описано выше (год доля начинаться 1 марта);
. вычислить день недели;
• выдать на экран результат.
Первое и второе действия очень просты и легко программируйте. Что касается выдачи на экран, то можно потребовать от программы, эта выдача была не просто числом от 0 до 6, а одной из строк «воскресенье», «понедельник»,..., «суббота». Для этого потребуются дополнительные усилия: нужно сначала создать массив строковых констант с именем, DAYS_OF_WEEK (дни недели), а затем выбрать из этого массива и выдать на экран нужную строку. Создать массив текстовых констант можно с объявления типизированной константы (см. гл. 7):
Const
Days.of_week: array [0..6] Of string [11] -
('воскресенье'.'понедельник','вторник',
'среда','четверг','пятница','суббота'},
В результате получим следующую процедуру:
PROCEDURE WrlteDay (d, m, y: Integer):
const
Days.of.week: array [0..6] of string[11] -
('воскресенье', 'понедельник','вторник',
'среда', ‘четверг', ‘пятница', ‘суббота');
Var
с, w: Integer;
BEGIN
If m<3 then
Begin
m: = m+10;
У:= У-1
End
else
m: = m-2;
C: =y div 100; {Найти столетие}
y: =y mod 100; {Найти год в столетии}
w: =abs (trunc (2.6*m-0.2]+d+y dlv 4 +y+c dlv 4-2*c) mod 7;
wrlteln (Days_of_week [w])
END;
12.2. Биоритмы
Давно известно, что творческая и физическая активность человека остается постоянной, циклически меняется, причем периодичность ее из нения приблизительно согласуется с периодом вращения Луны вокруг 3емли. Существует теория, согласно которой физическая, эмоциональная интеллектуальная активность человека подчиняется соответствующим биоритмам. Каждый биоритм представляет собой синусоиду со постоянным периодом, причем для каждого биоритма существует период. В отдельные дни все три биоритма человека могут достиг своего максимума и тогда человек испытывает подъем творческих и физических сил, в такие дни у него все спорится, он легко решает проблемы которые в другое время ему решить гораздо сложнее. Точно также существуют и «черные» дни, соответствующие спаду всех трех биоритмов.
Используя уже опробованную методику нисходящего программирования, создадим программу, в которой запрашивается дата рождения человека и дата, для которой требуется оценить его состояние. Программа должна рассчитать и выдать на экран ближайшие к этой дате дни пика и спада биоритмов.
Алгоритм программы можно укрупнено записать следующим образом:
. ввести дату рождения и текущую дату, проконтролировать их правильность и непротиворечивость;
. вычислить количество дней между двумя датами, чтобы определить фазу синусоид для текущей даты;
. вычислить количество дней от текущей даты до даты ближайшего пика биоритмов и даты ближайшего спада;
. определить и напечатать обе даты.
Будем считать, что каждое из перечисленных действий реализуется в отдельной процедуре, тогда начальный вариант программы будет таким:
Var
d0,d, {Дни рождения и текущий}
m0, m, {Месяцы рождения и текущий)
у0,у, {Годы рождения и текущий)
dmin, {Наименее благоприятный день)
dmax, {Наиболее благоприятный день)
days: Integer; {Количество дней от рождения)
PROCEDURE lnputDates{var dO.mO,yO.d,m,у : Integer);
{ Ввод даты рождения и текущей даты. Контроль правильности дат и их непротиворечивости (текущая дата должна быть позже даты рождения) }
BEGIN {InputDates}
END; {InputDates)
PROCEDURE Get_numbers_of_days (dO, mO, y0.d.m.y: Integer; var days: Integer);
{ Определение полного количества дней, прошедших от одной даты до другой }
BEGIN (Get_numbers_of_days)
END; {Get_numbers_of_days)
PROCEDURE FlndMaxMln (var dmin.dmax: integer; days: Integer);
(Поиск критических дней }
Begin {FindMaxMin}
End {FindMaxMin}
PROCEDURE WriteDates(dmin, dmax, days : Integer);
Определение критических дат по количеству дней, прошедших С момента рождения, и выдача этих дат на экран)
Begin {WriteDates}
End{WriteDates}
{Главная программа)
inputDates(dO.mO.yO.d.m.y);
Get_numbers_of_days(d0.mO,yO,d,m,y,days);
FindMaxMin( dmin.dmax, days);
WrteDates(dmin, dmax, days)
End.
Начинаем детализацию программы.
Прежде всего, давайте подумаем, как по
двум датам вычислить разделяющее их
количество дней вспомнить, что следует
учитывать неодинаковое количество дней
по месяцам года, а также 29 февраля для
високосных лет, то ответ на этот вопрос
окажется не таким уж простым.
Предлагаемый алгоритм подсчета
количества дней заключается в вычислении
количества дней от даты рождения до конца
месяца, а затем и года рождения,
количества дней от начала текущего года
до текущего месяца и текущей даты, а также
- в подсчете количества полных лет,
разделяющих обе даты. Количество лет
затем легко пересчитывается в количество
дней с учетом длины года (365 дней для
обычных и 366 дней для високосных лет). Это
очень прямолинейный алгоритм, но,
откровенно говоря, мне не пришло в голову
ничего. Возможно, существует более
изящный способ подсчета и Вы его знаете,
тогда программная реализация будет
другой.
Упростить алгоритм можно за счет
создания и использования массива из 12
целых чисел, содержащего количество дней
по месяцам не високосного года, т.е. 31, 28, 31,
30 и т.д. Этот массив (назовем его SIZE_OF_MONTH -
длина месяца) можно использовать и для
обратной задачи, т.е. для определения даты
критических дней, а также для проверки
правильности вводимых дат. Таким образом,
массив SIZE_OF_MONTH будет использоваться
сразу в трех процедурах. Сделаем его
глобальным, для чего его описание
поместим перед описанием процедур:
Const Slze_of_Month (31, 28, 31, 30);
array [1.. 12] of byte -30, 31, 30, 31, 31, 30, 31,);
Var dO.d, mO,m, ÓÎ.ó, dmin, dmax, days : integer;
PROCEDURE lnputDates(var dO.mO.yO,d.m.y : integer);
Поскольку описание массива размещается до описания процедур, он становится доступным внутри каждой из процедур и служит для них глобальным. В отличие от этого все константы и переменные, объявляемые внутри некоторой процедуры, являются локальными и могут исследоваться только в этой процедуре.
С учетом сказанного напишем следующий начальный вариант программной реализации процедуры
INPUTDATES:
PROCEDURE lnputDate«(var dO.mO,yO,d,m,у : integer); { Ввод даты рождения и текущей даты. Контроль правильности дат и их непротиворечивости (текущая дата должна быть позже даты рождения) }
var
correctly : Boolean; { Признак правильного ввода }
BEGIN {InputDates}
repeat
{ Ввести и контролировать дату рождения dO.mO.yO.}
{ Ввести и контролировать текущую дату d.m.y.}
{ Проверить непротиворечивость дат: }
correctly := у>уО;
If not correctly and (y=yO) then
begin
correctly := m>mO;
If not correctly and (m=mO) then correctly := d>=dO
end
until correctly
END; {InputDates}
В этой процедуре дважды выполняется одно и то же алгоритмическое действие (ввод и контроль даты). Это действие можно вынести в отдельную внутреннюю процедуру с именем INPDATE, тогда получим следующий окончательный вариант:
PROCEDURE lnputDates(var dO.mO.yO,d.m.y : Integer);
{ Ввод даты рождения и текущей даты. Контроль правильности дат и их непротиворечивости (текущая дата должна быть позже даты рождения) }
var correctly : Boolean; { Признак правильного ввода }
PROCEDURE lnpDate(text : string;var d.m.y : integer);
{ Выводит приглашение TEXT, вводит дату в формате ДД ММ ГГГГ и проверяет ее правильность }
const
YMIN = 1800; { Минимальный правильный год }
УМАХ = 2000; { Максимальный правильный год }
BEGIN {InpDate}
repeat
write(text);
readln(d.m.y):
correctly := (y>=YMIN) and (Y<=YMAX) and (m>=1) and (m<=T2) and (d>0); If correctly then
If (m=2) and (d=29) and (y mod 4=0) then
{ Ничего не делать: это 29 февраля високосного года! }
else
correctly := d>SIze.of_Month[m];
If not correctly then writelnf('Ошибка в дате!')
until correctly
END: {InpDate}
BEGIN {InputDates}
Repeat
InpDate(‘ Введите дату рождения в формате ДД ММ ГГГГ:',d0,m0,y0);
InpDate (‘ введите текущую дату: ',d.m,y);
{ Проверить не противоречивость дат: }
correctly := у>уО;
if not correctly and (y=yO) then
begin
correctly := m>mO;
If not correctly and (m=mO) then correctly := d>=dO;
end
until correctly
END; {InputDates}
В самом общем виде алгоритм подсчета количества дней, разделяющих две даты, описанные выше. При его реализации следует учесть возможных варианта:
• месячный младенец (год и месяц обеих дат одинаков): количество дней находится простым вычитанием двух чисел;
• годовалый младенец (год обеих дат совпадает): количество дней (остаток дней в месяце рождения) + (количество дней в текущем месяце) + (количество дней в месяцах, разделяющих обе даты);
. общий вариант (отличаются года): количество дней - (количество дней от даты рождения до конца года) + (количество дней в разделяющие даты годах) + (количество дней от начала текущего года до текущей даты)
С учетом этого составим начальный вариант программной реализации процедуры GET_NUMBERS_OF_DAYS:
PROCEDURE Get-numbers-Of_days(dO,m0.yO,d,m,y : integer;var days : Integer):
{ Определение полного количества дней, прошедших от одной даты до другой }
PROCEDURE Variant2;
{ Подсчет количества дней в месяцах, разделяющих обе даты }
BEGIN {Variant2}
END; {Variant3}
PROCEDURE Variant3;
{ Подсчет количества дней в месяцах и годах, разделяющих обе даты }
BEGIN {Variant3}
END; {Variants)
BEGIN {Get_numbers_of_days}
If (y=yO) and (m=m0) then { Даты отличаются только днями: days := d=dO
else { Даты отличаются не только днями: }
begin
days := d + Size_of_Month[mO]-dO;
{Учесть количество дней в текущем месяце и количество дней до конца месяца рождения]
if (yO mod 4=0) and (mO=2) then inc(days):
{ Учесть високосный год }
if y=yO then
Variant2
{ Разница в месяцах одного и того же года }
else
Variant3
{ Даты отличаются годами }
end
END; {Get_numbers_of_days}
В этом фрагменте используется способ связи вспомогательных процедур VARIANT2 и VARIANTS с основной процедурой через глобальные переменные, которыми являются параметры обращения к основной процедуре. Вспомогательные процедуры удобнее всего реализовать на основе циклов WHILE:
PROCEDURE Variant2;
{ Подсчет количества дней в месяцах, разделяющих обе даты}
var
mm : integer;
BEGIN {Variant2}
mm:= mO;
whlle mm<m do
begin
days := days+Size_of_Month[mm];
if (mm-2) and (yO mod 4=0) then
inc(mm)
end
END; {Variant?}
PROCEDURE Variant3:
{ Подсчет количества дней в месяцах и годах, разделяющих обе даты }
var
mm, yy : Integer;
BEGIN (Variants}
mm := mO+1;
while mm<=12 do { Учесть остаток года рождения: }
begin
days :- days+Size_of_Month[mm];
If (mm=2) and (yO mod 4=0) then Inc(days);
inc(mm);
end:
yy := yO+1;
while yy<y do { Прибавить разницу лет: }
begin
days := days+365;
If yy mod 4=0 then Inc(days);
Inc(yy)
end;
mm := 1;
while mm<m do { Прибавить начало текущего года: }
begin
days := days+Size_of_Month[mm);
if (y mod 4=0) and (mm=2) then Inc(days);
Inс(mm)
end
END: {Variants}
В процедуре FINDMAXMIN осуществляется поиск критических дней, т.е. ближайших к текущей дате дней, для которых все три биоритма достигают своего максимума и минимума. Предполагается, что биоритмы изменяются по законам синуса от количества прожитых дней с периодами TF, ТЕ и TI соответственно для физической, эмоциональной и интеллектуальной активности человека. В программе приняты следующие периоды (в днях):
TF= 23.6884
ТЕ=28.4261
TI=33.1638
Самый простой алгоритм поиска заключается в том, чтобы вычислить значения сумм всех трех синусоид для текущего дня и для каждого из последующих дней на некотором заранее обусловленном интервале, например, в пределах месяца. Сопоставив результаты расчетов для каждого дня, нетрудно определить критические дни:
PROCEDURE FindMaxMin(var dmln, dmax : integer;days : integer):
(Поиск критических дней)
const
TF = 2*3.1416/23.6884; (Период физической активности}
ТЕ = 2*3.1416/28.4261; {Период эмоциональной активности}
TI = 2*3.1416/33.1638; {Период интеллектуальной активности}
INTERVAL = 30; {Интервал прогноза}
var
min, (Накапливает минимум биоритмов} .
max, {Накапливает максимум биоритмов}
x : real; {Текущее значение биоритмов}
I : Integer;
BEGIN {FindMaxMin}
max := sin(days*TF)+sin(days*TE)+sin(days*TI);
max; {Начальное значение минимума и максимума pat значению биоритмов для текущего дня}
dmin := days;
dmax := days;
for I := 0 to INTERVAL do
begin
x:=
sin((days+l)*TF)+sin((days+I)*TE)+sin((days+I)*TI);
If x>max then
begin max := x;
dmax := days + I
end
else If x<min then
begin
min := x;
dmIn := days+l
end
end;
END; {FindMaxMin}
При разработке алгоритма процедуры WRITEDATES, с которой на экран выводится результат работы программы, учтем, основные сложности будут связаны с определением новой даты по начальной дате и количеству прошедших дней. Этот расчет будет дважды - для даты пика и даты спада биоритмов, поэтому его вынести в отдельную процедуру WR1TEDATE. Кроме того, вряд ли вы откажетесь от возможности вывода на экран дополнительной информации о том, сколько полных дней, часов, минут и секунд разделяют дата рождения человека и текущую дату. Однако реализация этого вывода столь проста, как это может показаться на первый взгляд. Дело в том, что диапазон возможных значений данных типа INTEGER составляет от 32768 до +32767. Средняя продолжительность жизни человека - около 1 лет, т.е. 25550 дней. Это значение еще можно представить в перемен типа INTEGER, однако часы, минуты и тем более секунды средней продолжительности жизни далеко превышают этот диапазон. Чтобы учить вывод достоверных данных, необходимо расширить диапазон чтений целых чисел. Для этого в Турбо Паскале предусмотрен специальный тип данных LONGINT («длинный» целый), имеющий диапазон чтений от -2147483648 до +2147483647 (см. гл. 4). Поэтому в процедур WRITEDATES следует предусмотреть вспомогательную переменную этого типа, присвоить ей значение переменной DAYS и уже затем использовать «длинную» переменную для вычисления (и вывода) часов, мин секунд. В результате начальный вариант процедуры WRITEDATES может быть таким:
PROCEDURE WriteDatesfdmln.dmax,days : Integer);
{ Определение и вывод дат критических дней. Вывод дополнительной информации о количестве прожитых дней, часов, минут и секунд}
PROCEDURE WrlteDate(text : string; dd : Integer):
(Определение даты для дня DO от момента рождения, в глобальных переменных d. m и у имеется текучая дата, в переменной DAYS -количество дней, прошедших от момента рождения до текущей дате выводится сообщение TEXT и найденная дата в формате ДД-МЕС-ГГГГ}
BEGIN (WriteDate)
END {WriteDate}
var
Longdays : = longint; { 'Длинная целая переменная для часов, минут и секунд’}
Begin{WriteDatas}
Longdays:=days;
writeln( 'Прошло: ',longdays,' дней, ', longdays*24,' часов, longdays*24*60,' минут, ',longdays*24*60*60.' секунд' );
WriteDate('Наименее благоприятный день: ', dmin);
WriteDate('Наиболее благоприятный день: ', dmax)
END; {WrlteDates}
Реализация процедуры WRITEDATE не вызывает особых сложностей:
PROCEDURE WriteDate(text : string; dd : integer);
const
Names_of_Monthes : array [1..12] of strlng[3] =('янв','фев',’мар','апр','май','июн', 'июл','авг','сен','окт','ноя','дек');
var
dO.mO,yO,ddd : Integer;
BEGIN {WriteDate};
DO:=d;
MO:=m;
УО:=y;
Ddd:=days;
While ddd<>dd do
begin
inc(dO); {Нарастить число}
if (yO mod 4<>0) and (dO>Slze_of_Month[mO]) or (yO mod 4=0) and (dO=30) then
begin {Корректировать месяц}
dO := 1;
inc(mO);
If mO=13 then {Корректировать год}
begin
mO := 1;
Inc(yO)
end
end;
inc(ddd)
end;
writeln(text,dO,'=',Names_of_Monthes[mO],'=',yO)
END; {WriteDate}
12.3 Игра "НИМ"
Ним - одна из самых старых и увлекательных математических игр. Для игры в ниш необходим партнер (в ним играют вдвоем), стол и набор фишек. В качестве фишек обычно используются камешки или монетки укладываются в три ряда так, как показано на рис. 4.
Правила нима просты. Игроки по очереди забирают одну или не сколько фишек из любого ряда. Не разрешается за один ход брать фишку из нескольких рядов. Выигрывает тот, кто возьмет последнюю фишку (фишки).
Если Вы сыграете несколько партий в ним, то скоро заметите, что существует некоторая оптимальная последовательность ходов, которая гарантирует победу, если только Вы начинаете игру и первым ходом берете две фишки из первого ряда. Любой другой ход даст шанс Вашему сопернику, который в этом случае наверняка победит, если, в свою очередь, воспользуется оптимальной стратегией.
Полный анализ игры с обобщением на любое число рядов с любым числом фишек в каждом ряду впервые опубликовал в 1901 г. профессором математики из Гарвардского университета Чарльз Л.Бутон, который и назвал игру «ним» от устаревшей формы английских глаголов «стянуть», «украсть». Открытая им оптимальная стратегия основана на двоичной системе счисления и довольно проста. Каждую комбинацию фишек я назвал либо опасной, либо безопасной: если позиция, создавшаяся после очередного хода игрока, гарантирует ему победу, она называется безопасной, если такой гарантии нет - опасной. Бутон строго доказал, что любую опасную позицию всегда можно превратить в безопасную нужным ходом Наоборот, если перед очередным ходом игрока уже сложилась безопасная позиция, то любой его ход превращает позицию в опасную. Таким образом, оптимальная стратегия состоит в том, чтобы каждым ходом опасную позицию превращать в безопасную и заставлять противника «портить» ее Использование оптимальной стратегии гарантирует победу игроку только тогда, когда он открывает партию и начальная позиция фишек опасна или он делает второй ход, а начальная позиция безопасна.
Чтобы определить, опасна позиция или безопасна, нужно количество фишек в каждом ряду записать в двоичной системе счисления. Если сумма чисел в каждом столбце (разряде) равна
нулю или четна, позиция безопасна. Если же сумма нечетна хотя бы в одном разряде, то позиция опасна. Например, для начальной позиции по схеме 3-4-5 получим:
Десятичная запись количества фишек | Двоичная запись количества фишек |
3 | 011 |
4 | 100 |
5 | 101 |
Сумма по разрядам | 212 |
Сумма цифр в среднем столбце равна 1 - нечетному числу, что свидетельствует об опасности этой позиции. Поэтому первый игрок может сделать ее безопасной для себя, если он возьмет две фишки из первого ряда. В результате в первом ряду остается только 1 фишка (двоичное число также 1), и сумма чисел в среднем столбце изменится на ноль.
В привычной нам десятичной системе счисления емкость каждого разряда равна 10, а для записи значений разряда используются цифры от 0 до 9. В двоичной системе счисления емкость каждого разряда равна 2, а из всех цифр используются только 0 и 1. В этой системе число записывается в виде суммы степеней двойки и при переходе от одного разряда к соседнему левому вес разряда увеличивается в 2 раза. Если нужно записать число 2 в двоичной системе, следует действовать точно так же, как при записи числа 10 в десятичной системе: записать ноль в первом (младшем) разряде и единицу - слева от него, т.е. 10 в двоичной системе означает 2 в десятичной системе. Точно так же 100 в двоичной системе означает 4 в десятичной, 1000 - 8 и т.д.
Для перевода любого целого положительного числа из десятичной системы в двоичную можно использовать прием последовательного деления числа на 2. Например, для перевода десятичного числа в двоичную систему используется такая цепочка делении:
Если остатки отделения записать в обратном порядке, получим 1011 это и есть двоичное представление десятичного числа 11. В этом легко убедиться, записав двоичное число 10U как сумму степеней 2: 1.23 + 0.2Z+1.21+ 1.2°=11
Попробуем разработать программу, которая будет выполнять роль партнера в игре, причем это будет весьма опасный противник, так как он будет знать оптимальную стратегию и умело ею пользоваться.
Представим себе на минутку, что Вы уже создали программу и начинаете работу с ней. Как организовать удобное взаимодействие с программой? Конечно, возможно простейшее решение: Вы предварительно раскладываете на столе монетки, по запросу программы вводите в нее Ваш ход, затем читаете на экране ход программы, делаете нужные изменения в раскладке монет и т.д. Вряд ли Вас удовлетворит такая программа Гораздо эффектнее имитировать на экране игровое поле с фишками : своеобразное табло игры, в котором сообщается об очередных ходах противников. Однако использованные ранее средства вывода данных (процедуры WRITE ч WRITELN) недостаточны для этих целей, ведь с их помощью нельзя указать конкретное место на экране, куда нужно поместить выводимую информацию. Вывод этими процедурами всегда начинается с той позиции на экране, которую в данный момент занимает курсор. Следовательно, для вывода текста в нужное место экрана требуется пер обращением к этим процедурам изменить положение курсора. Для этих целей служит процедура GOTOXY, которая хотя и является стандартно располагается в отдельной библиотеке (модуле) с именем CRT. Подробнее о модулях мы поговорим в гл.9, а сейчас просто учтем, что процедуры и функции из дополнительных библиотек становятся доступны программе, если в самом ее начале объявить об их использовании. Так в указание об использовании библиотеки CRT делается таким образом:
Uses CRT;
После такого указания программе становятся доступны дополнительные процедуры и функции, с помощью которых можно организовать гибкое управление текстовым экраном, в том числе процедура СОТОХУ перемещающая курсор в произвольное место на экране.
Теперь попробуем составить алгоритм главной программы. В простейшем виде он таков:
Uses CRT; {Подключение библиотеки дополнительных процедур и функций для управления экраном}
var
exit : Boolean; {Признак окончания работы)
BEGIN {Главная программа}
{Подготовить экран к работе}
repeat
{Ввести, проконтролировать и отобразить ход игрока}
{Найти и отобразить ход программы}
until exit
END.
В этом алгоритме выделяются три главных действия и организуете цикл, который будет выполняться до тех пор, пока где-то в программе к переменной EXIT (выход) не будет присвоено значение TRUE.
Вначале экран подготавливается к работе: формируется игровое поле с фишками и выводится информация о правилах игры. Как уже говорилось, ним позволяет играть с произвольным количеством фишек. Разумно ввести в программу возможность, которая бы позволила пользователю самому указывать число рядов и количество фишек в рядах, т.е. настраивать программу на нужную раскладку фишек. Можно несколько модифицировать главную программу, чтобы предусмотреть эту возможность:
User CRT; {Подключение библиотеки дополнительных процедур и функций для управления экраном)
var
exit : Boolean; {Признак окончания работы)
change : Boolean; {Признак изменения условий игры)
PROCEDURE Prepare: { подготовка экрана к игре }
BEGIN {Prepare}
END; {Prepare}
PROCEDURE GetPlayerMove;
{ получить, проконтролировать и отобразить ход игрока }
BEGIN {GetPlayerMove}
END: {GetPlayerMove}
PROCEDURE SetOwnerMove;
{ Найти и отобразить очередной ход программы }
BEGIN {SetOwnerMove}
END; {SetOwnerMove}
________
BEGIN {Главная программа)
{Подготовить начальную расстановку фишек)
repeat { Цикл изменения условий игры }
Prepare; { Подготовить экран }
repeat { Игровой цикл }
GetPlayerMove; { Получить ход пользователя }
if not (exit or change) then
SetOwnerMove { Определить собственный ход }
until exit or change
until exit
END.
В этом варианте главная программа содержит два вложенных друг в друга цикла REPEAT...UNTIL: внутренний цикл управляет игрой, внешний отвечает за изменение условий игры. Оба цикла управляются двумя логическими переменными, которые являются глобальными для трех основных процедур PREPARE, GETPLAYERMOVE, SETOWNERMOVE и, следовательно, могут изменяться внутри этих процедур.
Теперь настал момент подумать о том, каким способом в программе будет храниться и использоваться информация о текущем состоянии игры. Судя по всему, нам понадобятся хотя бы две переменные: в одной, назовем ее NROW, будет содержаться число рядов фишек, в другой (NCOL) - количество фишек в каждом ряду. Переменная NROW содержит одно целое положительное число, поэтому ее тип должен быть INTEGER. В переменной NCOL должно быть не менее NROW целых чисел, т.е. ее тип - это массив целых чисел. Поскольку в программе предусмотрена возможность изменения условий игры самим игроком, переменная NRO W может меняться от партии к партии. В соответствии с этим должна была бы меняться и длина массива NCOL. Однако в Турбо Паскале нельзя использовать массивы, длина которых меняется динамически, т.е. в процессе работы программы. Эта длина должна определяться статически (на этапе компиляции) и не может меняться в работающей программе. Значит, понадобится массив достаточно большой длины, чтобы его хватило на все случаи. На экране одновременно можно отобразить максимум 25 строк по 80 символов в каждой строке. Однако использовал we строчки экрана как возможные ряды фишек вряд ли целесообразно: во-первых, сама игра при большом количестве рядов становится неинтересной, так как игрок не сможет проанализировать в уме все варианты ходов; во-вторых, на экране не останется места для вывода другой информации. Будем считать, что максимальное количество рядов фишек не должно превышать 14. Укажем это константой MAXROW- теперь, если Вы захотите назначить другое максимальное количество рядов, понадобится изменить значение этой константы и перекомпилировать программу. Именно таким способом программам придается дополнительная гибкость: Вы сосредоточиваете в нескольких константах параметры, которые
выбраны Вами произвольно и которые Вы или кто-то другой, возможно
захочет изменить. Все размерности массивов или другие особенности программной реализации следует определять через эти константы, тогда процедура переделки программы предельно упростится. С учетом сказанного назначим следующие глобальные константы и переменные:
const
MAXROW = 14; {Максимальное количество рядов}
MAXCOL = 20; {Максимальное количество фишек в ряду}
type
ColType = array [1..MAXROW] of integer;
var
exit: Boolean; (Признак окончания работы}
Change: Boolean; (Признак изменения условий игры}
nrow : Integer; 'Количество рядов}
ncol : ColType; {Максимальное количество фишек по рядам)
col : ColType; (Текущее количество фишек по рядам}
Константа MAXCOL не участвует в формировании массивов, будет использоваться для контроля горизонтальных размеров игрового поля. Поэтому она, а также пять переменных сделаны глобальными. Можно считать, что начальная раскладка фишек соответствует схеме 3-4-5, таx можно написать такой окончательный вариант главной программы:
Uses CRT; {Подключение библиотеки дополнительных процедур и функций для управления экраном}
const
MAXROW = 14; (Максимальное количество рядов}
MAXCOL= 20; (Максимальное количество фишек в ряду}
type
ColType = array [1..MAXROW] of integer;
var
exit : Boolean; (Признак окончания работы}
change : Boolean; 'Признак изменения условий игры}
nrow : integer; {Количество рядов}
ncol : ColType; Максимальное количество фишек по рядам}
col : ColType; (Текущее количество фишек по рядам}
{——————————————}
PROCEDURE Prepare;
{ Подготовка экрана к игре }
BEGIN (Prepare}
END: {Prepare}
PROCEDURE GetPlayerMove;
{ Получить, проконтролировать и отобразить ход игрока }
BEGIN {GetPlayerMove}
END; {GetPlayerMove}
PROCEDURE SetOwnerMove;
{ Найти и отобразить очередной ход программы }
BEGIN {SetOwnerMove}
END; {SetOwnerMove}
BEGIN {Главная программа}
nrow := 3; { Подготовить игру }
nсо[1] := 3; { на поле из трех }
псо1[2] := 4; { рядов фишек }
лсоЦЗ] := 5; { по схеме 3-4-5. }
repeat { Цикл изменения условий игры }
Prepare; { Подготовить экран }
repeat { Игровой цикл }
GetPlayerMove; { Получить ход пользователя }
If not (exit or change) then
SetOwnerMove { Определить собственный ход }
untiI exit or change anti I exit
END.
Приступим к конструированию процедуры PREPARE. В ходе работы формируется значение переменной COL, соответствующее и начальной раскладке фишек, и выводится информация о правилах игр Чтобы было понятнее дальнейшее описание программной реализации, рис. 5 показан вид экрана в начальном состоянии игры.
Рис. 5. Вид экрана в начале игры ним
Процедура начинает свою работу с очистки экрана от имеющейся на нем информации. Это достигается обращением к стандартной процедуре без параметров CLRSCR. Затем выводятся три строчки с названием игр и кратким описанием ее правил. Кроме того, слева и справа на экран
формируются заголовки для двух колонок цифр, в которых затем буду отображаться номер ряда (слева) и текущее количество фишек в ряд (справа). Эта информация поможет игроку сообщить программе свой ход.
Для размещения информации на нужных участках экрана используете процедура GOTOXY(X,Y), с помощью которой курсор перемещаете нужным образом. Параметры X и Y этой процедуры задают новые координаты курсора. Начало координат соответствует точке (1,1) и размещается в левом верхнем углу экрана, поэтому горизонтальные координаты увеличивается слева направо, а вертикальная - сверху вниз.
PROCEDURE Prepare:
{ Подготовка данных и экрана к игре }
const
Headero ='ИГРА НИМ';
'leaden - 'вы можете взять любое число фишек из любого ряда.1:
Header2 * 'Выигрывает тот, кто возьмет последнюю фишку.';
Header3 - 'Номер ряда';
Header4 - 'Количество фишек';
var
I : Integer;
BEGIN {Prepare}
clrscr; { Очистить экран }
{ Вывести заголовок: }
GotoXY((80-length(HeaderO)) dlv 2,1);
write(HeaderO):
GotoXY((80-Ungtl)(Header1)) div 2,2);
wrlte(Headerl):
GotoXY((80-Length(Header2)) div 2.3);
writeln(Header2);
write(Header3);
GotoXY(80-Length(Header4),4):
write(Headerl): ncol[i ]
{ Подготовить начальную раскладку:
for i := 1 to nrow do col [I ] END: {Prepare}
Для вывода верхних строк строго посередине экрана используется задание горизонтальной координаты курсора для процедуры СОТОХУ как половины от разницы между полной длиной экрана (80 позиций) длиной выводимой строки (определяется с помощью функции LENGTH) В процедуре GETPLAYERMOVE осуществляются ввод, контрольное отображение на экране очередного хода игрока. Предварительно нужно показать игроку текущее состояние игрового поля. Поскольку поле бур обновляться как минимум дважды (после хода игрока и после хода программы), действия, связанные с изображением поля на экране, следует вынести в отдельную процедуру. Назовем ее SHOW FIELD и займемся реализацией.
Судя по всему, нам понадобится организовать цикл; в ходе цикла каждого ряда игрового поля будет выведена строка, в левой части которой указывается номер ряда, в правой - текущее количество фишек в нем, посередине выводятся символы, имитирующие фишки. В принципе можно выбрать любой символ ПК для обозначения фишки, напримерQ или О. Я предпочел воспользоваться символом псевдографики с ко 220: этот символ (см. прил. 2) представляет собой небольшой квадратик и легко ассоциируется с финкой.
PROCEDURE ShowFleltl:
{ Отображает на экране текущее состояние игрового поля }
const
FISH = #220; {Символ-указатель фишки }
ХО = 4; {Левая колонка номеров рядов }
XI = 72; {Правая колонка количества фишек }
X = 20; {Левый край игрового поля }
Var
I,J : Integer;
BEGIN {Showfleld}
for I := 1 to nrow do
begin
GotoXY(XO, I+4);
wrlte(l); { Номер ряда }
GotoXY(X1,I+4); wrlte(col[l ]:2); ( Количества фишек в ряду }
for J := 1 to ncol[i ] do ( Вывод ряда фишек:}
begin
GotoXY(X+2*J,I+4);
if J[i] then write(FISH) else wrlte(‘.')
end
end
END { ShowFieId}
Символы FISH (квадратики) выводятся через одну позицию, чтобы не сливались на экране. В те позиции, в которых ранее стояли уже снятые с пола фишки, выводится точка.
Теперь вернемся к процедуре GETPLAYERMOVE. При вводе любого очередного хода игрок должен задать два целых числа XI и Х2. Первое из них указывает номер ряда, а второе - количество фишек, которые игрок хочет забрать из этого ряда. Программа должна проконтролировать правильность задания этих чисел: X1 должно указывать непустой ряд, Х2 не может превышать количество фишек в этом ряду. Кроме того, мы должны условиться о двух особых случаях:
. пользователь больше не хочет играть и дает команду завершить работу программы;
. пользователь хочет изменить условия игры.
Пусть ввод числа X1=0 означает команду выхода из программы, а X1= -1 - команду изменения условий игры. Тогда можно написать такой начальный вариант процедуры:
PROCEDURE GetPlayerMove;
{ Получить, проконтролировать и отобразить ход игрока }
var
correctly : Boolean; {Признак правильности сделанного хода}
x1,x2 : Integer; {Вводимый код}
BEGIM ;GetPlayerMove}
ShowfieId; {Показать начальное состояние игрового поля}
{ Сообщить игроку правила ввода хода }
repeat
{ Пригласить игрока ввести ход }
readln(x1,x2); {Ввести очередной код}
exit:=x1=0; {Контроль команды выхода}
change :=х1=-1; {Контроль команды изменения}
if not (exit or change) then
{ Проверить правильность хода и установить нужное значение
переменной CORRECTLY. Если ход правильный, сделать нужные изменения в раскладке фишек и показать поле. }
еlse
corrесtlу := true {Случай EXIT или CHANGE}
until correctly;
if change then
{Изменить условия игры }
END. {GatPtayerMove}
В этом варианте в процедуре GETPLAYERMOVE нет описания процедуры SHOWFIELD. Сделано это не случайно: процедура SHOWFIELD может понадобиться также и при реализации процедуры SETOWNERMOVE, поэтому она должна быть глобальной по отношению и к GETPLAYERMOVE и к SETOWNERMOVE, т.е. ее описание должно в тексте программы предшествовать описаниям двух использующих ее процедур. Действия
{ Сообщить игроку правила ввода хода },
{ Пригласить игрока ввести ход }
{ Проверить правильность хода и установить нужное значение
переменной CORRECTLY. Если ход правильный, сделать нужные
изменения в раскладке фишек и показать поле. }
не очень сложны в реализации, поэтому их можно осуществить непосредственно в теле процедуры CETPLAYERMOVE. Иное дело - изменение условий игры. Это действие полезно реализовать в отдельной процедуре] CETCHANGE. С учетом этого второй вариант процедуры] GETPLAYERMOVE примет такой вид:
PROCEDURE GetPlayerMove:
{ Получить, проконтролировать и отобразить ход игрока }
const
ТЕХТ1 = 'Введите Ваш ход в формате РЯД КОЛИЧ
ТЕХТ01 ='(например, 23- взять из 2 ряда 3 фишки)';
ТЕХТ2 ='или введите 0 0 для выхода из игры; ':
ТЕХТ02 = '-1 0 для настройки игры':
ТЕХТЗ = 'Ваш ход: ':
Y = 20;
var
correctly : Boolean; {Признак правильности сделанного хода}
х1.х2 : Integer: (Вводимый ход}
ROCEGURE GetChange;
{ Ввести новую настройку игры (количество рядов и количество фишек в каждом ряду}
{Номер строки для вывода сообщений)
BEGIN {GetChange}
END: {GetChange}
BEGIN {GetPlayerMove}
ShowField; { показать начальное состояние поля }
{ Сообщить игроку правила ввода хода: }
GotoXY((80-Length(TEXT1*TEXT01) div 2,Y);
write(TEXT1+TEXT01):
GotoXY((80-Length(TEXT2+TEXT02)) div 2.Y+1): write(TEXT2+TEXT02): repeat "
{ Пригласить игрока ввести ход: }
GotoXY(1,Y+2):
wr Ite(TEXT3): {Вывести приглашение и стереть предыдущий ход}
GotoXY(WhereX-16.Y+2); (курсор влево на 16 позиций)
readln(x1,x2); (Ввести очередной ход}
exit := х1-0: (Контроль команды выхода}
change := х1--1; {Контроль команды изменения)
if not (exit or change) then
begin
correctly := (x1>0) and (x<=nrow) and
(x2<=col(x1]) and (x2>0);
If correctly then
begin {Ход правильный:}
col[x1] := col[x1]-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 возвращает текущую горизонтальную координату куpcopa, а функция WHEREY- его вертикальную координату). Сделано это для того, чтобы в случае, если игрок ввел неверный ход и программа повторяет вывод приглашения, пробелы в константе ТЕХТЗ затерли бы строку предыдущего ввода.
Чтобы завершить создание процедуры GETPLAYERMOVE, нужно спроектировать процедуру GETCHANGE, в которой осуществляется изменение условий игры. Я привожу текст этой процедуры без пояснений и приглашаю Вас самостоятельно разобраться в том, как она работает:
PROCEDURE GetChange:
{ ввести новую настройку игры (количество рядов и количество фишек в каждом ряду } с
cons t
t1 -'Н А С Т Р О И К А ИГРЫ';
t2 -'(ввод количества рядов и количества фишек в каждом ряду}':
var
correctly : Boolean;
i : integer;
Begin {GetChange}
cirscr;
GotoXY((80-Length(t1)) div 2,1);
wr i t e(t1);
GotoXY((80-Length(t2)) div 2,2);
wr i t e (12);
repeat
GotoXY(1,3);
write('Введите количество рядов (максимум '.MAXROW, '):
GotoXY{WhereX-6.WhereY);
read I n(nrow) ;
correctly := (nrow<=MAXROW) and (nrow>1):
if not correctly then
write(#7)
until correctly;
for I :- 1 to nrow do
repeat
GotoXY(1,i+3);
write(' ряд ',!,', количество фишек (максимум ', MAXCOL,');
GotoXY(WhereX-6,WnereY);
readln(ncol[i]);
correctly := (ncol[i]<=MAXCOL) and (ncol(i]>0);
if not correctly then
wrlte(>7)
unt i I correct ly
End {GetChange}
Переходим к конструированию процедуры SetOwnerMove, в которой программа должна проконтролировать текущую ситуацию на игровом поле и выбрать собственный ход. Работа процедуры начинается с подсчета числа непустых рядов. В зависимости от этого подсчета реализуются следующие действия:
• если все ряды пусты, значит предыдущим ходом игрок забрал последнюю фишку и он победил; нужно поздравить его с победой, усложнить игру и предложить сыграть еще раз;
• если есть только один непустой ряд, то очередной ход программки очевиден - забрать все фишки, что означает победу машины: сообщить с этом и предложить сыграть еще раз;
• если осталось два или более непустых ряда, выбрать собственный ход на основе оптимальной стратегии. Начальный вариант процедуры:
PROCEDURE SetOwnerMove;
{ Найти и отобразить очередной ход программы }
FUNCTION CheckField : Integer;
{ Проверка состояния игры. Возвращает 0, если нет ни одной фишки (победа игрока), 1 - есть один ряд (победа машины) и - количество непустых рядов в остальных случаях }
BEGIN {CheckField}
END; {CheckField}
PROCEDURE PlayerVictory:
{ Поздравить игрока с победой и усложнить игру }
BEGIN {PlayerVictory}
END: (PlayerVictory}
PROCEDURE OwnVlctory;
( Победа машины }
BEGIN {OwnVictory}
END; {OwnVlctory}
PROCEDURE ChooseMove:
j Выбор очередного хода }
BEGIN {ChooseMove}
END; {ChooseMove}
{Проверить количество непустых ря-
jBce ряды пусты - победа игрока)
{Один непустой ряд - победа машины}
{Выбрать очередной ход}
BEGIN {SetOwnerMove}
case CheckField of дов}
0 : PlayerVictory
1 ; : OwnVictory
else
ChooseMove;
end; {case}
END; {SetOwnerMove}
Функция CHECKFIELD и процедуры PLAYERVICTORY OWNVICTORY'достаточно просты и их текст помещается без каких-либо пояснений в окончательный вариант программы (см. прил.5.3). лишь, что в случае победы игрока нет смысла повторять партию заново той же самой раскладкой фишек. Поэтому игра усложняется: в исход раскладку добавляется еще по одной фишке в каждый ряд.
В процедуре CHOOSEMOVE анализируется позиция и выбирает очередной ход программы. Описание оптимальной стратегии уже приводилось выше. Действия программы заключаются в поиске первого еле (старшего) двоичного разряда, для которого сумма чисел нечетная, такой разряд не обнаружен, то текущая позиция безопасна для игрока, значит любой ход программы сделает ее опасной. В этом случае программы не существует оптимального выбора и она лишь убирает одну фишку из любого непустого ряда. Такая тактика означает пассивное ожидание ошибки игрока.
Если обнаружен разряд с нечетной суммой, программа приступает к реализации оптимальной стратегии и тогда игрок обречен на поражение. Для выбора ряда, из которого следует взять фишки, программа продемонстрирует последовательно все ряды и отыскивает тот ряд, количество фишек в котором (в двоичном представлении) дает единицу в разряде ; Значение этого разряда для количества фишек в ряду заменяется нулем Затем программа продолжает подсчет суммы для оставшихся младших разрядов. Если в каком-либо из них вновь обнаружена нечетность, значение этого разряда для количества фишек в ряду инвертируется, т.е. ( заменяется на 1, а 1 на 0. Например, если двоичные представления числа фишек и четности сумм таковы:
число фишек в ряду: 01001
четность сумм: 01011
(единицей указаны разряды с нечетными суммами), то в результате этой операции получим:
число фишек в ряду: 00010
четность сумм: 00000
Таким образом, в исходном состоянии в ряду было 1001 - 9 фишек, безопасная позиция требует, чтобы в ряду осталось 0010 - 2 фишки, следовательно, из него нужно забрать 9-2-7 фишек.
Окончательный вариант программы представлен в прил.5.3. Попробуйте разобраться в ее деталях самостоятельно.
В программной реализации алгоритма широко используется то обстоятельство, что Ваш компьютер, как и все остальные вычислительные машины, работает с числами, представленными в двоичной системе счисления. Поэтому для получения двоичного представления числа в процедуре BITFORM оно проверяется на четность с помощью стандартной функции ODD, затем сдвигается вправо на один двоичный разряд (операция SHR), вновь осуществляется проверка на четность и т.д. до тех пор, пока не будут проверены все разряды. Максимальное число двоичных разрядов достаточное для двоичного представления количества фишек в ряду MAXCOL-63 задается константой BIT-6.
Для получения суммы двоичных разрядов в процедуре CHOOSEMOVE используется суммирование разрядов по модулю 2 с помощью операции XOR. Такое суммирование дает 0, если количество единиц четное или равно нулю, и 1 - если нечетное. В этой же процедуре для инверсии двоичного разряда применяется оператор
If nbltflJ-1 then
ncb!t[j,i] := ord(ncbit[j, I]-0); {Инверсия разрядов}, в котором используется соглашение о внутреннем представлении логических величин в Турбо Паскале: 0 соответствует FALSE, а 1 - TRUE.