Процедуры и функции
Процедуры и функции представляют собой важный инструмент Турбо Паскаля, позволяющий писать хорошо структурированные программы. В структурированных программах обычно легко прослеживается основной алгоритм, их нетрудно понять любому читателю, они проще в отладке и менее чувствительны к ошибкам программирования. Все эти свойства являются следствием важной особенности процедур (функций), каждая из которых представляет собой во многом самостоятельный фрагмент программы, связанный с основной программой лишь с помощью нескольких параметров. Самостоятельность процедур (функций) позволяет локализовать в них все детали программной реализации того или иного алгоритмического действия и поэтому изменение этих деталей, например, в процессе отладки обычно не приводит к изменениям основной программы. Многие примеры в этой книге невелики по размерам (не более 30-40 строк), поэтому написать такие программы можно и без процедур. Иное дело - создание крупных программ в сотни, тысячи и десятки тысяч строк. Писать такие программы как нечто единое целое, без расчленения на относительно самостоятельные фрагменты, т.е. без структурирования, просто невозможно. Практически во всех языках программирования имеются средства структурирования. Языки, в которых предусмотрены такие механизмы, называются процедурно-ориентированными. К их числу принадлежит и Турбо Паскаль. Процедурой в Турбо Паскале называется особым образом оформленный фрагмент программы, имеющий собственное имя. Упоминание этого имени в тексте программы приводит к активизации процедуры и называется ее вызовом. Сразу после активизации процедуры начинают выполняться входящие в нее операторы, после выполнения последнего из них управление возвращается обратно в основную программу и выполняются операторы, стоящие непосредственно за оператором вызова процедуры (рис.2.2).
Рис.2.2. Взаимодействие вызывающей программы и процедуры Для обмена информацией между основной программой и процедурой используется один или несколько параметров вызова. Как мы увидим дальше (см. гл. 8), процедуры могут иметь и другой механизм обмена данными с вызывающей программой, так что параметры вызова могут и не использоваться. Если они есть, то они перечисляются в круглых скобках за именем процедуры и вместе с ним образуют оператор вызова процедуры. Функция отличается от процедуры тем, что результат ее работы возвращается в виде значения этой функции, и, следовательно, вызов функции может использоваться наряду с другими операндами в выражениях. С примерами процедур и функций мы уже сталкивались - это стандартные процедуры чтения И записи READ, READLN, WRITE, WRITELN, функции ORD, CHR, математические функции и др. Стандартными они называются потому, что созданы одновременно с системой Турбо Паскаль и являются ее неотъемлемой частью. В Турбо Паскале имеется много стандартных процедур и функций. Наличие богатой библиотеки таких программных заготовок существенно облегчает разработку прикладных программ. Однако в большинстве случаев некоторые специфичные для данной прикладной программы действия не находят прямых аналогов в библиотеках Турбо Паскаля, и тогда программисту приходится разрабатывать свои, нестандартные процедуры и функции. Нестандартные процедуры и функции необходимо описать, чтобы компилятор мог установить связь между оператором вызова и теми действиями, которые предусмотрены в процедуре (функции). Описание процедуры (функции) помещается в разделе описаний и внешне выглядит как программа, но вместо заголовка программы фигурирует заголовок процедуры (функции).
Не вдаваясь в дальнейшие подробности, попробуем составить собственную процедуру, чтобы пояснить сказанное. Пусть в этой процедуре преобразуется некоторая символьная строка таким образом, чтобы все строчные буквы заменялись соответствующими прописными. В Турбо Паскале имеется стандартная функция UPCASE (см. гл.4), которая выполняет аналогичные действия над одиночным символом. Наша процедура (назовем ее UPSTRING) будет преобразовывать сразу все символы строки, причем сделаем ее пригодной не только для латинских букв, но и для букв русского алфавита. Разработку программы проведем в два этапа. Сначала сконструируем основную (вызывающую) часть программы. Ее действия очень просты: она должна ввести входную строку (назовем ее Sinp) с клавиатуры, преобразовать ее с помощью процедуры UpString в выходную строку Sout и напечатать результат. Эти действия нетрудно запрограммировать, например: Program CharsConvert; Procedure UpString(si: String; var s2: String); begin {UpString} s2:= s1 {Пока еще нет преобразования!} end; {UpString} var Sinp, Sout: String; {Исходная и преобразованная строки} begin {Начало основной (вызывающей) программы} Write('Введите строку: '); ReadLn(Sinp); {Вводим исходную строку} UpString(Sinp,Sout); {Преобразуем ее.к прописным буквам} WriteLn (' Результат: ',Sout) {Печатаем результат} end. {Конец вызывающей программы} В этой программе используется замещение процедуры UPSTRING так называемой «заглушкой», т.е. процедурой, в которой на самом деле не осуществляется нужных нам действий, а выходная строка просто копирует входную. (Однако эта программа синтаксически абсолютно правильна и при желании ее можно запустить на счет.) Заглушка понадобилась нам по двум причинам. Во-первых, приведенная программа очень проста, в ней отсутствует детальная реализация процедуры и это позволяет наглядно проиллюстрировать механизм ее описания. Во-вторых, на ее примере мы знакомимся с универсальным методом конструирования сложных программ, получившим название нисходящее программирование. В соответствии с этим методом создание программы начинается «сверху», т.е. с разработки самого главного, генерального алгоритма. На верхнем уровне обычно еще не ясны детали реализации той или иной части программы, поэтому эти части следует заменить временными заглушками. Желательно, чтобы временный вариант программы был синтаксически правильным, тогда можно его откомпилировать и убедиться в отсутствии в нем синтаксических ошибок. Такой прогон даст определенную уверенность перед разработкой и реализацией алгоритмов нижнего уровня, т.е. перед заменой заглушек реально работающими процедурами. Если реализуемый в заглушке алгоритм достаточно сложен, его вновь структурируют, выделяя главный алгоритм и применяя новые заглушки, и т.д. Процесс продолжается «вниз» до тех пор, пока не будет создан полностью работоспособный вариант программы.
В дальнейшем мы еще не раз будем использовать метод нисходящего программирования, а сейчас вернемся к описанию нашей процедуры. Как видим, это описание начинается зарезервированным словом Procedure, за которым следуют имя процедуры и список формальных параметров. Список параметров заключается в круглые скобки и содержит перечень параметров с указанием их типа. Заметим, что перед параметром s2, с помощью которого в вызывающую программу возвращается результат преобразования, стоит зарезервированное слово VAR. Именно таким способом компилятору указываются те параметры, в которых процедура возвращает вызвавшей ее программе результат своей работы (подробнее см. гл. 8). Зарезервированное слово Procedure, имя процедуры и список ее параметров образуют заголовок процедуры. За заголовком следует тело процедуры, содержащее новый раздел описаний (этот раздел пока еще пуст) и раздел исполняемых операторов (оператор s2: = s1). Приступим к разработке алгоритма процедуры. Для этого обратимся к таблице кодировки символов, используемой в ПК (см. прил. 2). В соответствии с этой таблицей коды символов латинских строчных букв от а до z образуют непрерывный массив монотонно нарастающих чисел от 97 до 122, а коды соответствующих им прописных букв - непрерывный массив чисел от 65 до 90. Преобразование строчных латинских букв в прописные, следовательно, состоит в уменьшении кода буквы на 32. Сложнее обстоит дело с символами русского алфавита (кириллицей). В зависимости от принятого способа кодировки русские строчные буквы могут образовывать один сплошной массив (кодировки ГОСТ и MIC), два массива (альтернативная кодировка), несплошной массив (кодировка типа ЕСТЕЛ), неупорядоченный массив (кодировка КОИ-8). Если исключить два последних варианта кодировки, использовавшихся на устаревших ПК, то задача преобразования буквы состоит в том, чтобы к внутреннему коду русской буквы А (для букв от а до п) или к коду буквы Р (для букв от р до я) прибавить разницу в кодах текущего символа и кодах букв а и и. Например, если преобразуется буква б, то к коду А нужно прибавить разницу между кодами а и б, т.е. единицу, в результате получим код буквы Б. Точно так же при преобразовании буквы ф к коду буквы П будет прибавлено число 5 (как разница кодов ф и п), поэтому в результате получится код буквы Ф. С учетом этого можно составить следующий алгоритм реализации процедуры: для каждого символа исходной строки s1 определить, к какому подмассиву a...z, a...p или п...я принадлежит код этого символа, и затем изменить его, добавив к кодам букв А (латинская), А (русская) или Я соответствующую разницу. Если символ не принадлежит ни к какому из подмассивов, нужно поместить его код в выходную строку без изменений.
Вот возможный вариант процедуры: Procedure UpString(sl: String; var s2: String); var i: Integer; {Счетчик цикла преобразования} с: Char; {Рабочая переменная преобразования} begin {UpString} s2:= ' '; {Вначале выходная строка пуста} {Цикл посимвольного анализа исходной строки} for i:= 1 to Length(si) do begin {Берем из входной строки очередной символ} с:= sl[i]; {Проверяем символ на принадлежность к одному из трех подмассивов} if (с >= 'a') and (с <= 'z') then с:= chr(ord('A')+ord(c)-ord('a1)) {А,а - латинские!} else if (c >= 'a') and (c <= 'n') then с:= chr(ord('A')+ord(с)-ord('a')) {А,а - русские!} else if (c >= 'p') and (с <= 'я') then с:= chr(ord('PI)+ord(c)-ord('p')); s2:= s2+c end end; {UpString} В процедуре вначале с помощью оператора s2:= ''; подготавливается «пустая» выходная строка, т.е. строка нулевой длины. Затем используется цикл от 1 до длины входной строки s1 (эта длина получается с помощью стандартной функции Length), в ходе которого проверяется принадлежность очередного символа указанным подмассивам и осуществляется необходимая коррекция его внутреннего кода. Для доступа к отдельным символам строки используется замечательное свойство типа данных STRING, позволяющее рассматривать строку как набор (массив) символов. Первый символ этого набора имеет индекс 1, второй - 2 и т.д. Индекс указывается сразу за именем строки в квадратных скобках. Таким образом, s1 [i] -это i-ый символ строки s1. Преобразованный символ добавляется в конец выходной строки.
Добавив комментарии и поместив тело процедуры вместо заглушки в первоначальный вариант программы, получим окончательно ее рабочий вариант (пример 2.10). Пример 2.10 Program CharsConvert; {Программа вводит произвольную текстовую строку, преобразует все входящие в нее буквы в прописные и печатает результат преобразования} PROCEDURE UpString(sl: String; var s2: String); {Эта процедура преобразует буквы входной строки si в прописные буквы латинского или русского алфавита и помещает результат преобразования в выходную строку s2. Используется предположение о том, что последовательности латинских букв от «а» до «z» и русских букв. от «а» до «п» и от «р» до «я», а также последовательности соответствующих им прописных букв образуют непрерывные массивы} var i: Integer; {Счетчик цикла преобразования} с: Char; {Рабочая переменная преобразования} begin {UpString} s2:=' '; {Вначале выходная строка пуста} {Цикл посимвольного анализа исходной строки} for i:= 1 to Length(s1) do begin {Берем из входной строки очередной символ} с:= s1[i]; {Проверяем символ на принадлежность к одному из трех подмассивов} if (с >= 'a') and (с <= 'z') then с:= chr(ord('А')+ord(c)-ord('a')) {A,a - латинские!} else if (c >= 'a') and (c <= 'n') then с:= chr(ord('A')+ord(c)-ord('a')) {A,a -русские!} else if (c >= 'p') and (с <= 'я') then с:= chr(ord('P')+ord(c)-ord('p')); s2:= s2+c end end; {UpString} var Sinp, Sout: String; {Исходная и преобразованная строки} begin { Начало основной (вызывающей) программы} Write('Введите строку: '); ReadLn(Sinp); {Вводим исходную строку} UpString(Sinp,Sout); {Преобразуем ее к прописным буквам} WriteLn(' Результат: ',Sout) {Печатаем результат} end. {Конец вызывающей программы} Рассмотрим иной способ реализации той же программы: оформим алгоритм преобразования в виде функции. Кроме того, с помощью стандартной функции UPCASE преобразуем каждый очередной символ (это преобразование осуществляется только для букв латинского алфавита) и тем самым исключим проверку принадлежности символа к строчным латинским буквам: Function UpString(s1: String): String; var i: Integer; с: Char; s2: String; {Результат преобразования} begin {UpString} s2:= ' '; for i:= 1 to Length(si) do begin {Получаем и преобразуем очередной символ} с:= UpCase(si [i]); if (с >= 'a') and (с <= 'п') then с:= chr(ord('A')+orcKcJ-ord('a')) else if (c >= 'p') and (с <= 'я') then с:= chr(ord('P')+ord(c)-ord('p')); s2:= s2+c end; UpString:= s2 {Присваиваем значение функции UpString} end; {UpString} var Sinp: String; begin {Начало основной программы} Write('Введите строку: '); ReadLn(Sinp); WriteLn(' Результат: ',UpString(Sinp)) end. {Конец основной программы} Программа получилась несколько проще за счет того, что функцию можно использовать в качестве параметра обращения к другой процедуре (в нашем случае к WriteLn). Обратите внимание: в теле любой функции нужно осуществить присваивание ей вычисленного значения (см. оператор UpString:= s2). В левой части оператора присваивания в этом случае указывается имя функции. ПРИМЕРЫ ПРОГРАММ Мы познакомились с основными возможностями языка Турбо Паскаль. Как видите, ядро языка очень компактно и отличается простотой - именно в этом состоит главная заслуга автора Паскаля Н.Вирта: язык, придуманный им, прост и естественен, он легко осваивается, на нем не трудно писать самые разнообразные программы. Конечно, рассмотрены далеко не все свойства Турбо Паскаля, ведь его главная отличительная черта - это богатство типов данных. Однако уже рассмотренного вполне достаточно для написания многих полезных программ. Приводимые ниже программы относительно сложны, поэтому они реализуются поэтапно, по методу нисходящего программирования. Мне кажется, что тем читателям, кто не имеет большого опыта в программировании или кто захочет подробнее ознакомиться с нисходящим программированием, изучение этой главы принесет определенную пользу. Если Вам будет скучно разбираться в «кухне» программирования, но Вас заинтересуют описываемые здесь программы и Вы захотите их повторить, то в прил.5 Вы найдете полный текст соответствующей программы; однако в каждой из них используются некоторые дополнительные возможности языка Турбо Паскаль, которые не рассматривались ранее и которые обсуждаются в пропущенных Вами фрагментах книги. При оформлении программ я стремился использовать хороший стиль написания программ, т.е. такую их форму, которая дает наиболее полное представление о структуре программы в целом и ее отдельных частей. Не существует какого-либо стандарта, определяющего хороший стиль программы. Обычно это интуитивное понятие включает способ расположения операторов и описаний по строкам (не рекомендуется размещать более одного оператора на каждой строке), а также выделение отступами тела составных и условных операторов. Последнее особенно важно в программах Турбо Паскаля: сплошь и рядом в них встречаются операторные скобки begin... end, причем часто вложенные друг в друга; использование отступа служит дополнительным средством проверки правильности их расстановки - не случайно в редакторе среды предусмотрена соответствующая опция. Принятый мною стиль оформления программ не претендует на эталон, просто мне кажется, что таким образом оформленные программы читаются лучше. Если Вы всерьез намерены программировать на Турбо Паскале, имеет смысл составить собственное представление о хорошем стиле и далее неукоснительно придерживаться его - очень скоро некоторые дополнительные издержки на подготовку программ с лихвой окупятся их «читабельностью», а это поможет Вам вспомнить все детали реализации программы, которая была написана несколько месяцев тому назад. 2.7.1. Вычисление дня недели Случалось ли Вам мучительно вспоминать, какой именно день недели приходился на то или иное число год или два назад, или вычислять, на какой день недели в этом году приходится Ваш день рождения? Если да, то Вас, думаю, заинтересует простая программа, позволяющая по заданной дате мгновенно вычислить соответствующий день недели. В ее основе лежит такая формула: день недели = остаток от деления X на 7, где X = abs(trunc(2.6*m-0.2)+d+y/4+y+c/4-2*c); m - номер месяца (см. ниже); d - число (день месяца); с - номер столетия (см. ниже); у - номер года в столетии. При использовании этой формулы следует учесть два обстоятельства. Во-первых, формула верна для григорианского календаря нового стиля (от 1582 до 4903 года). Во-вторых, год и месяц следует предварительно преобразовать так, как если бы начало года приходилось на 1 марта. Иными словами, март в этой формуле имеет порядковый номер 1, апрель 2,..., январь 11 и февраль 12, причем январь и февраль следует отнести к предыдущему году. Например, для 1 февраля 1991 года номер месяца должен быть равен 12, а год 1990, в то время как для 31 декабря 1991 года номер месяца - 10, а год - 1991. Результат вычисления дается в виде целого числа в диапазоне от 0 до 6, причем 0 соответствует воскресенью. Приступим к разработке программы. Прежде всего, предположим, что программа уже создана и Вы осуществляете ее прогон. Какая форма взаимодействия с программой кажется Вам наиболее подходящей? Вряд ли Вас удовлетворит однократное ее исполнение (ввод некоторой даты и вывод на экран соответствующего дня недели). Скорее всего Вы захотите повторить работу программы для нескольких дат, например, поинтересоваться, в какой день недели Вы родились, затем, на какой день недели приходится в этом году Ваш день рождения, дни рождения близких, друзей; может быть, определить, в какой день родились известные Вам исторические деятели, и т.д. Таким образом, в программе следует предусмотреть многократное выполнение действий <ввод даты> - <вычисление дня недели>, причем число циклов вычисления заранее не известно. Сразу же возникает новый вопрос: как сообщить программе, что Вы завершаете работу с ней? Для этого можно условиться, что ввод некоторой заранее обусловленной или недопустимой даты должен интерпретироваться программой, как указание на прекращение работы. С учетом сказанного, напишем такой начальный вариант программы: var IsCorrectDate: Boolean; {Признак правильной даты} d,m,y: Integer; {Вводимая дата - день, месяц и год} begin repeat {Ввести в переменные d, л? и у очередную дату и проверить ее. Если дата правильная, установить IsCorrectDate=True, иначе IsCorrectDate=False} if IsCorrectDate then {Вычислить и выдать на экран день недели}; until not IsCorrectDate end. Если Вы попытаетесь запустить эту программу на счет, то ее поведение будет зависеть от начального значения переменной IsCorrectDate. Это значение случайно, так как компилятор Турбо Паскаля не проводит начальной инициализации переменных. Скорее всего, тот байт оперативной памяти, в котором она разместится, окажется нулевым, что в Турбо Паскале расценивается как логическое значение FALSE, поэтому с большой вероятностью ничего не произойдет, и программа сразу же завершит свою работу (условие not IsCorrectDate будет выполнено). Если начальное значение IsCorrectDate окажется не нулевым, то цикл REPEAT...UNTIL будет выполняться до тех пор, пока Вы не выключите компьютер или не нажмете клавиши Ctrl-Break. Будем считать, что необходимые действия осуществляются в двух процедурах с именами InputDate (ввод даты) и WriteDay (вычисление и печать дня недели). В процедуру InputDate не нужно ничего передавать из программы, так как в ней самой осуществляются ввод и контроль даты. Поэтому заголовок процедуры может иметь такой вид: Procedure InputDate(var d,m,y: Integer; var correctly: Boolean); Процедура WriteDay, напротив, только получает из программы нужные ей данные и ничего не возвращает в программу, поэтому в ее заголовке параметры описываются без слова VAR: Procedure WriteDay(d,m,у: Integer); С учетом этого программу можно уточнить следующим образом: var IsCorrectDate: 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 {WriteDay} end; {WriteDay} {..............................} begin repeat InputDate(d,m,y,IsCorrectDate); if IsCorrectDate then WriteDay(d,m,y) until not IsCorrectDate end. Теперь можно разработать процедуру INPUTDATE. Ввод даты не вызывает трудностей - стандартные процедуры WRITE и READLN отлично приспособлены для этой цели. Для проверки правильности даты нужно проверить принадлежность месяца диапазону 1...12 и года - диапазону 1582...4903. Кроме того, число не должно выходить из диапазона 1...31. Если Вы не очень настаиваете на более точной проверке числа в зависимости от месяца и года (для февраля), то программная реализация процедуры будет следующей: Procedure InputDate(var d,m,y: Integer; var correctly: Boolean); {Вводит в переменные d, m и у очередную дату и проверяет ее. Если дата правильная, устанавливает correctly=true, иначе correctly=false } begin {InputDate} Write('Введите дату в формате ДД ММ ГГ: '); ReadLn(d,m,y); correctly:= (d>=l)and (d<=31) and (m>=l) 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] = ('воскресенье','понедельник','вторник', 'среда','четверг','пятница','суббота'); В этом объявлении идентификатор Days_of_week описывается в разделе констант, однако справа от него указан тип данных (массив строк), как если бы описывалась переменная, а уже только после типа стоит знак равенства и заключенный в круглые скобки список элементов массива. В результате получим следующую процедуру: Procedure WriteDay(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; {Остальные месяцы} с:= у div 100; {Вычисляем столетие} у:= у mod 100; {Находим год в столетии} w:= abs(trunc(2.6*m-0.2)+d+y div 4+y+c div 4-2*c) mod 7; WriteLn(Days_of_week[w]) end; Окончательный вариант программы приведен в прил.5.1. 2.7.2. Биоритмы Давно известно, что творческая и физическая активность человека не остается постоянной, циклически меняется, причем периодичность ее изменения приблизительно согласуется с периодом вращения Луны вокруг Земли. Существует теория, согласно которой физическая, эмоциональная и интеллектуальная активность человека подчиняется соответствующим биоритмам. Каждый биоритм представляет собой синусоиду со строго постоянным периодом, причем для каждого биоритма существует свой период. В отдельные дни все три биоритма человека могут достигнуть своего максимума и тогда человек испытывает подъем творческих и физических сил, в такие дни у него все спорится, он легко решает проблемы, которые в другое время ему решить гораздо сложнее. Точно также существуют и «черные» дни, соответствующие спаду всех трех биоритмов. Используя уже опробованную методику нисходящего программирования, создадим программу, в которой запрашивается дата рождения человека и дата, для которой требуется оценить его состояние. Программа должна рассчитать и выдать на экран ближайшие к этой дате дни пика и спада биоритмов. Алгоритм программы можно укрупнено записать следующим образом: · ввести дату рождения и текущую дату, проконтролировать их правильность и непротиворечивость; · вычислить количество дней между двумя датами, чтобы определить фазу синусоид для текущей даты; · вычислить количество дней от текущей даты до даты ближайшего пика биоритмов и даты ближайшего спада; · определить и напечатать обе даты. Будем считать, что каждое из перечисленных действий реализуется в отдельной процедуре, тогда начальный вариант программы будет таким: Procedure InputDates(var dO,mO,yO,d,m,y: Integer); {Вводит дату рождения и текущую дату. Контролирует правильность дат и их непротиворечивость (текущая дата должна быть позже даты рождения) } begin {InputDates} end; {InputDates} {..........................} Procedure Get_count_pf_days (dO,mO,yO,d,m,y: Integer; var days: Integer); {Определяет полное количество дней, прошедших от одной даты до другой} begin {Get_count_of_days} end; {Get_count_of_days} {--------------------------} Procedure FindMaxMin (var dmin,dmax: Integer; days: Integer); {Ищет критические дни} begin {FindMaxMin} end; {FindMaxMin} {--------------------------} Procedure WriteDates (dmin, dmax, days: Integer); {Определяет критические даты по количеству дней, прошедших от момента рождения, и выдает эти даты на экран} begin {WriteDates} end; {WriteDates} {--------------------------} var d0,d, {Дни рождения и текущий} m0,m, {Месяцы рождения и текущий} у0,у, {Годы рождения и текущий} dmin, {Наименее благоприятный день} dmax, {Наиболее благоприятный день} days: Integer; {Количество дней от рождения} begin {Главная программа} Input-Dates (d0,m0,y0,d,m,y); Get_numbers_of_days (d0,m0,y0,d,m,y,days); FindMaxMin (dmin, dmax, days); WriteDates (dmin, dmax, days) end. Начинаем детализацию программы. Прежде всего подумаем, как по двум датам вычислить разделяющее их количество дней? Если вспомнить, что следует учитывать неодинаковое количество дней по месяцам года, а также 29 февраля для високосных лет, то ответ на этот вопрос окажется не таким уж простым. Предлагаемый алгоритм подсчета количества дней заключается в вычислении количества дней от даты рождения до конца месяца, а затем и года рождения, количества дней, от начала текущего года до текущего месяца и текущей даты, а также - в подсчете количества полных лет, разделяющих обе даты. Количество лет затем легко пересчитывается в количество дней с учетом длины года (365 дней для обычных и 366 дней для високосных лет). Это очень прямолинейный алгоритм, но, откровенно говоря, мне не пришло в голову ничего другого. Возможно, существует более изящный способ подсчета и Вы его знаете, тогда программная реализация будет другой. Упростить алгоритм можно за счет создания и использования массива из 12 целых чисел, содержащего количества дней по месяцам невисокосного года, т.е. 31, 28, 31, 30 и т.д. Этот массив (назовем его SIZE_OF_MONTH - длина _месяца) можно использовать и для обратной задачи, т.е. для определения даты критических дней, а также для проверки правильности вводимых дат. Таким образом, массив SIZE__OF_MONTH будет использоваться сразу в трех процедурах. Сделаем его глобальным, для чего его описание поместим перед описанием процедур: const Size_of_Month: array - [1..12] of Byte = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); {--------------------------} Procedure InputDates (var d0,m0,y0,d,m,y: Integer); ......... Поскольку описание массива размещается до описания процедур, он становится доступным внутри каждой из процедур и служит для них глобальным. В отличие от этого все константы и переменные, объявляемые внутри некоторой процедуры, являются локальными и могут использоваться только в этой процедуре. С учетом сказанного напишем следующий начальный вариант программной реализации процедуры INPUTDATES: Procedure InputDates(var d0,m0,y0,d,m,y: Integer); {Вводит дату рождения и текущую дату. Контролирует правиль- ность дат и их непротиворечивость (текущая дата должна быть позже даты рождения)} var correctly: Boolean; {Признак правильного ввода} begin {InputDates} repeat {Вводим и контролируем дату рождения d0,m0,y0.} {Вводим и контролируем текущую дату d,m,y.} {Проверяем непротиворечивость дат:} correctly:= у > у0; if not correctly and (у = y0) then begin correctly:= m > m0; if not correctly and (m = m0) then correctly:= d>=d0 end until correctly end; {InputDates} В этой процедуре дважды выполняется одно и то же алгоритмическое действие (ввод и контроль даты). Это действие можно вынести в отдельную внутреннюю процедуру с именем INPDATE, тогда получим следующий окончательный вариант: Procedure InputDates(var d0,m0,y0,d,m,y: Integer); {Вводит дату рождения и текущую дату. Контролирует правильность дат и их непротиворечивость (текущая дата должна быть позже даты рождения)} var correctly: Boolean; {Признак правильного ввода} {--------------------------} Procedure InpDate (text: String; var d,m,y: Integer); {Выводит приглашение TEXT, вводит дату в формате ДД ММ ГГГГ и проверяет ее правильность} const YMIN = 1800; {Минимальный правильный год} YМАХ = 2000; {Максимальный правильный год} begin {InpDate} repeat Write (text); ReadLn (d,m,y); correctly:= (y >= YMIN) and (Y <= YMAX) and (m >= 1) and (m <= 12) 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 WriteLn (' Ошибка в дате!') until correctly end; {InpDate} {--------------------------} begin {InputDates} repeat InpDate ('.Введите дату рождения в формате ДД ММ ГГГГ:',d0,m0,y0); InpDate (' Введите текущую дату: ',d,m,y); {Проверяем непротиворечивость дат:} correctly:= у > у0; if not correctly and (y = y0) then begin correctly:= m > m0; if not correctly and (m = m0) then correctly:= d >= d0 end until correctly end; {InputDates} В самом общем виде алгоритм подсчета количества дней, разделяющих две даты, описан выше. При его реализации следует учесть три возможных варианта: · месячный младенец (год и месяц обеих дат одинаков): количество дней находится простым вычитанием двух чисел; · годовалый младенец (год обеих дат совпадает): количество дней = (остаток дней в месяце рождения) + (количество дней в текущем месяце) + (количество дней в месяцах, разделяющих обе даты); · общий вариант (отличаются года): количество дней = (количество дней от даты рождения до конца года) + (количество дней в разделяющих даты годах) + (количество дней от начала текущего года до текущей даты). С учетом этого составим начальный вариант программной реализации процедуры GET_NUMBERS_OF_DAYS: Procedure Get_numbers_of_days (d,m,y,d,m,y: Integer; var days: Integer); {Определение полного количества дней, прошедших от одной даты до другой } {--------------------------} Procedure Variant2; {Подсчет количества дней в месяцах,разделяющих обе даты} begin {Variant2} end; {Variant2} {--------------------------} Procedure Variant3; {Подсчет количества дней в месяцах и годах, разделяющих обе даты} begin {Variant3} end; {Variant3} {--------------------------} begin {Get_numbers_of_days} if (y = y0) and (m = m0) then {Даты отличаются только днями: } days:= d - d0 else {Даты отличаются не только днями:} begin days:= d + Size_of_Month [m0] - d0; {Учитываем количество дней в текущем месяце и количество дней до конца месяца рождения} if (y0 mod 4=0) and (m0 = 2) then inc(days); {Учитываем високосный год} if у = y0 then Variant2 {Разница в месяцах одного и того же года} else Variant3 {Даты отличаются годами} end end; {Get_numbers_of_days} В этом фрагменте используется способ связи вспомогательных процедур VARIANT2 и VARIANT3 с основной процедурой через глобальные переменные, которыми являются параметры обращения к основной процедуре. Вспомогательные процедуры удобнее всего реализовать на основе циклов WHILE: Procedure Variant2; {Подсчет количества дней в месяцах, разделяющих обе даты } var mm: Integer; begin {Variant2} mm: = m0; while mm < m do begin days:= days + Size_of_Month [mm]; if (mm = 2) and (y0 mod 4=0) then inc (days); inc (mm) end end; {Variant2} {--------------------------} Procedure Variant3; {Подсчет количества дней в месяцах и годах, разделяющих обе даты } var mm/ УУ: Integer; begin {Variant3} mm: = m0 + 1; while mm <= 12 do {Учитываем остаток года рождения:} begin days:= days+Size_of_Month [mm]; if (mm = 2) and (y0 mod 4=0) then inc (days); inc (mm) end; yy:= y0 + 1; while yy < у 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); inc (mm) end end; {Variant3} В процедуре FINDMAXMIN осуществляется поиск критических дней, т.е. ближайших к текущей дате дней, для которых все три биоритма достигают своего максимума и минимума. Предполагается, что биоритмы изменяются по законам синуса от количества прожитых дней с периодами ТF, ТE и TI соответственно для физической, эмоциональной и интеллектуальной активности человека. В программе приняты следующие периоды (в днях): Знакомство с языком Турбо Паскаля TF= 23.6884 ТЕ= 28.4261 TI= 33.1638 Самый простой алгоритм поиска заключается в том, чтобы вычислить значения сумм всех трех синусоид для текущего дня и для каждого из последующих дней на некотором заранее обусловленном интервале, например, в пределах месяца. Сопоставив результаты расчетов для каждого дня, нетрудно определить критические дни: Procedure FindMaxMin(var dmin,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); min:= max; {Начальное значение минимума и максимума равно значению биоритмов для текущего дня} dmin:= days; dmax:= days; for i:= 0 to INTERVAL do begin x:= sin((days+i)*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 + i end end; end; {FindMaxMin} При разработке алгоритма процедуры WRITEDATES, с помощью которой на экран выводится результат работы программы, учтем, что основные сложности будут связаны с определением новой даты по начальной дате и количеству прошедших дней. Этот насчет будет повторяться дважды - для даты пика и даты спада биоритмов, поэтому его следует вынести в отдельную процедуру WRITEDATES. Кроме того, вряд ли Вы откажетесь от возможности вывода на экран дополнительной информации о том, сколько полных дней, часов, минут и секунд разделяют дату рождения человека и текущую дату. Однако реализация этого вывода не столь проста, как это может показаться на первый взгляд. Дело в том, что диапазон возможных значений данных типа INTEGER составляет от -32768 до +32767. Средняя продолжительность жизни человека - около 70 лет, т.е. 25550 дней. Это значение еще можно представить в Переменной типа INTEGER, однако часы, минуты и тем более секунды средней продолжительности жизни далеко превышают этот диапазон. Чтобы получить вывод достоверных данных, необходимо расширить диапазон значений целых чисел. Для этого в Турбо Паскале предусмотрен специальный тип данных LONGINT («длинный» целый), имеющий диапазон значений от -2147483648 до +2147483647 (см. гл. 4). Поэтому в процедуре WRITEDATES следует предусмотреть вспомогательную переменную этого типа, присвоить ей значение переменной DAYS и уже затем использовать «длинную» переменную для вычисления (и вывода) часов, минут, секунд. В результате начальный вариант процедуры WRITEDATES может быть таким: Procedure WriteDates (dmin,dmax,days: Integer); {Определение и вывод дат критических дней. Вывод дополнительной информации о количестве прожитых дней, часов, минут и секунд } {---------------------} Procedure WriteDate (text: String; dd: Integer); {Определение даты для дня DD от момента рождения. В глобальных переменных d, m и у имеется текущая дата, в переменной DAYS -количество дней, прошедших от момента рождения до текущей даты.Выводится сообщение TEXT и найденная дата в формате ДД-МЕС-ГГГГ} begin {WriteDate} end; {WriteDate} {---------------------} var LongDays: Longlnt; {"Длинная" целая переменная для часов,минут и секунд } begin {Wri teDates} LongDays: = days; WriteLn('Прошло: ', LongDays,' дней, ', longDays*24, ' часов, ', LongDays*24*60, ' минут, ', LongDays*24*60*60, ' секунд'); WriteDate ('Наименее благоприятный день: ', drain); WriteDate ('Наиболее благоприятный день: ',dmax) end; {WriteDates} Реализация процедуры WRITEDATE не вызывает особых сложностей: Procedure WriteDate (text: String; dd: Integer); const Names_of_Monthes: array [1..12] of String [3] =('янв','фев','мар','апр','мая', 'июн','июл','авг','сен','окт', 'ноя','дек'); var d0,m0,y0,ddd: Integer; begin {WriteDate} d0:= d; m0:= m; y0: = y; ddd:= days; while ddd<>dd do begin inc(d0); {Наращиваем число} if (y0 mod 4 <> 0) and (d0 > Size_of_Month[m0]) or (y0 mod,4=0) and (d0=30) then begin {Корректируем месяц} d0:= 1; inc(m0); if m0 = 13 then {Корректируем год} begin m0:= 1; inc(y0) end end; inc(ddd) end; WriteLn(text,d0,'-',Names_of_Monthes[m0],'-',y0) end; {WriteDate } Собрав воедино отдельные части, получим полный текст программы (прил.5.2), предназначенной для определения биоритмов. 2.7.3. Игра ним Ним - одна из самых старых и увлекательных математических игр. Для игры в ним необходим партнер (в ним играют вдвоем), стол и набор фишек. В качестве фишек обычно используются камешки или монетки. В наиболее известном варианте нима 12 фишек раскладываются в три ряда так, как показано на рис. 2.3. Рис.2.3. Фишки, расположенные для игры в ним по схеме 3-4-5 Правила нима просты. Игроки по очереди забирают одну или несколько фишек из любого ряда. Не разрешается за один ход брать фишки из нескольких рядов. Выигрывает тот, кто возьмет последнюю фишку (фишки). Если Вы сыграете несколько партий в ним, то скоро заметите, что существует некоторая оптимальная последовательность ходов, которая гарантирует победу, если только Вы начинаете игру и первым ходом берете две фишки из первого ряда. Любой другой ход даст шанс Вашему сопернику, который в этом случае наверняка победит, если, в свою очередь, воспользуется оптимальной стратегией. Полный анализ игры с обобщением на любое число рядов с любым числом фишек в каждом ряду впервые опубликовал в 1901 г. профессор математики из Гарвардского университета Чарльз Л.Бутон, который и назвал игру «ним» от устаревшей формы английских глаголов «стянуть», «украсть». Открытая им оптимальная стратегия основана на двоичной системе счисления и довольно проста. Каждую комбинацию фишек Бутон назвал либо опасной, либо безопасной: если позиция, создавшаяся после очередного хода игрока, гарантирует ему победу, она называется безопасной, если такой гарантии нет - опасной. Бутон строго доказал, что любую опасную позицию всегда можно превратить в безопасную нужным ходом. Наоборот, если перед очередным ходом игрока уже сложилась безопасная позиция, то любой его ход превращает позицию в опасную. Таким образом, оптимальная стратегия состоит в том, чтобы каждым ходом опасную позицию превращать в безопасную и заставлять противника «портить» ее. Использование оптимальной стратегии гарантирует победу игроку только тогда, когда он открывает партию и начальная позиция фишек опасна или он делает второй ход, а начальная позиция безопасна. Чтобы определить, опасна позиция или безопасна, нужно количество фишек в каждом ряду записать в двоичной системе счисления. Если сумма чисел в каждом столбце (разряде) равна нулю или четна, позиция безопасна. Если же сумма нечетна хотя бы в одном разряде, то позиция опасна. Например, для начальной позиции по схеме 3-4-5 получим:
Сумма по разрядам 212 Сумма цифр в среднем столбце равна 1 - нечетному числу, что свидетельствует об опасности этой позиции. Поэтому первый игрок может сделать ее безопасной для себя, если он возьмет две фишки из первого ряда. В результате в первом ряду остается только 1 фишка (двоичное число также 1), и сумма чисел в среднем столбце изменится на ноль. В привычной нам десятичной системе счисления емкость каждого разряда равна 10, а для записи значений разряда используются цифры от 0 до 9. В двоичной системе счисления емкость каждого разряда равна 2, а из всех цифр используются только 0 и 1. В этой системе число записывается в виде суммы степеней двойки и при переходе от одного разряда к соседнему левому вес разряда увеличивается в 2 раза. Если нужно записать число 2 в двоичной системе, следует действовать точно так же, как при записи числа 10 в десятичной системе: записать ноль в первом (младшем) разряде и единицу - слева от него, т.е. 10 в двоичной системе означает 2 в десятичной системе. Точно так же 100 в двоичной системе означает 4 в десятичной, 1000 - 8 и т.д. Для перевода любого целого положительного числа из десятичной системы в двоичную можно использовать прием последовательного деления числа на 2. Например, для перевода десятичного числа 11 в двоичную систему используется такая цепочка делении:
Если, начиная с последнего результата, остатки от деления записать в обратном порядке, получим 1011 - это и есть двоичное представление десятичного числа 11. В этом легко убедиться, записав двоичное число 1011 как сумму степеней 2: 1х23+1х22+1х21+1 = 11 Попробуем разработать программу, которая будет выполнять роль партнера в игре, причем это будет весьма опасный противник, так как он будет «знать» оптимальную стратегию и умело ею пользоваться. Представим себе на минутку, что Вы уже создали программу и начинаете работу с ней. Как организовать удобное взаимодействие с программой? Конечно, возможно простейшее решение: Вы предварительно раскладываете на столе монетки, по запросу программы вводите в нее Ваш ход, затем читаете на экране ход программы, делаете нужные изменения в раскладке монет и т.д. Вряд ли Вас удовлетворит такая программа. Гораздо эффектнее имитировать на экране игровое поле с фишками и своеобразное табло игры, в котором сообщается об очередных ходах противников. Однако использованные ранее средства вывода данных (процедуры WRITE и WRITELN) недостаточны для этих целей, ведь с их помощью нельзя указать конкретное место на экране, куда нужно поместить выводимую информацию. Вывод этими процедурами всегда начинается с той позиции на экране, которую в данный момент занимает курсор. Следовательно, для вывода текста в нужное место экрана требуется перед обращением к этим процедурам изменить положение курсора. Для этих целей служит процедура GOTOXY, которая хотя и является стандартной, но располагается в отдельной библиотеке (модуле) с именем CRT. Подробнее о модулях мы поговорим в гл.9, а сейчас просто учтем, что процедуры и функции из дополнительных библиотек становятся доступны в программе, если в самом ее начале объявить об их использовании. Так, указание об использовании библиотеки CRT делается таким образом: Uses CRT; После такого указания программе становятся доступны дополнительные процедуры и функции, с помощью которых можно организовать гибкое управление текстовым экраном, в том числе процедура GOTOXY, перемещающая курсор в произвольное место на экране. Теперь попробуем составить алгоритм главной программы. В простейшем виде он таков: Uses CRT; {Подключение библиотеки дополнительных процедур и функций для управления экраном} var exit: Boolean; {Признак окончания работы} begin {Подготовить экран к работе} repeat {Ввести, проконтролировать и отобразить ход игрока} {Найти и отобразить ход программы} until exit end. В этом алгоритме выделяются три главных действия и организуется цикл, который будет выполняться до тех пор, пока где-то в программе переменной EXIT (выход) не будет присвоено значение TRUE. Вначале экран подготавливается к работе: формируется игровое поле с фишками и выводится информация о правилах игры. Как уже говорилось, ним позволяет играть с произвольным количеством фишек. Разумно ввести в программу возможность, которая бы позволила пользователю самому указывать число рядов и количество фишек в рядах, т.е. настраивать программу на нужную раскладку фишек. Можно несколько модифицировать главную программу, чтобы предусмотреть эту возможность: Uses 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 целых чисел, т.е. ее тип - это массив целых чисел. Поскольку в программе предусмотрена возможность изменения условий игры самим игроком, переменная NROW может меняться от партии к партии. В соответствии с этим должна была бы меняться и длина массива NCOL. Однако в Турбо Паскале нельзя использовать массивы, длина которых меняется динамически, т.е. в процессе работы программы. Эта длина должна определяться статически (на этапе компиляции) и не может меняться в работающей программе. Значит, понадобится массив достаточно большой длины, чтобы его хватило на все случаи. На экране одновременно можно отобразить максимум 25 строк по 80 символов в каждой строке. Однако использовать все строчки экрана как возможные ряды фишек вряд ли целесообразно: во-первых, сама игра при большом количестве рядов становится неинтересной, так как игрок не сможет проанализировать в уме все варианты ходов; во-вторых, на экране не останется места для вывода другой информации. Будем считать, что максимальное количество рядов фишек не должно превышать 14. Укажем это константой MAXROW - теперь, если Вы захотите назначить другое максимальное количество рядов, понадобится изменить значение этой константы и перекомпилировать программу. Именно таким способом программам придается дополнительная гибкость: Вы сосредоточиваете в нескольких константах параметры, которые выбраны Вами произвольно и которые Вы или кто-то другой, возможно, захочет изменить. Все размерности массивов или другие особенности программной реализации следует определять через эти константы, тогда процедура переделки программы предельно упростится. С учетом сказанного назначим следующие глобальные константы и переменные: const MAXROW = 14; {Максимальное количество рядов} MAXCOL = 20; {Максимальное количество фишек в ряду} type ColType= array [I..MAXROW] of Integer; var exit:Boolean; {Признак окончания работы} change:Boolean; {Признак изменения условий игры} nrow:Integer; {Количество рядов} ncol:ColType; {Максимальное колич-во фишек по рядам} col:ColType; {Текущее количество фишек по рядам} Константа MAXCOL не участвует в формировании массивов, она будет использоваться для контроля горизонтальных размеров игрового поля. Поэтому она, а также пять переменных сделаны глобальными. Если считать, что начальная раскладка фишек соответствует схеме 3-4-5, то можно написать такой окончательный вариант главной программы: Uses CRT; {Подключение библиотеки дополнительных процедур и функций для управления экраном}
{------------------------} Procedure Prepare; {Готовит экран к игре} begin {Prepare} end; {Prepare} {------------------------} Procedure GetPlayerMove; {Получает, контролирует и отображает ход игрока} begin {GetPlayerMove} end; {Get PlayerMove} {------------------------} Procedure SetOwnerMove; {Находит и отображает очередной ход программы} begin {SetOwnerMove} end; {SetOwnerMove} {------------------------} begin {Главная программа} nrow:= 3; {Готовим игру... } ncol [1]:= 3; { на поле из трех } ncol [2]:= 4; { рядов фишек } ncol [3]:= 5; { по схеме 3-4-5.} repeat {Цикл изменения условий игры} Prepare; {Подготовить экран} repeat {Игровой цикл} GetPlayerMove; {Получить ход пользователя} if not (exit or change) then SetOwnerMove {Определить собственный ход} until exit or change until exit end. Приступим к конструированию процедуры PREPARE. В ходе ее работы формируется значение переменной COL, соответствующее начальной раскладке фишек, и выводится информация о правилах игры. Чтобы было понятнее дальнейшее описание программной реализации, на рис. 2.4 показан вид экрана в начальном состоянии игры. Процедура начинает свою работу с очистки экрана от имеющейся на нем информации. Это достигается обращением к стандартной процедуре без параметров CLRSCR. Затем выводятся три строчки с названием игры и кратким описанием ее правил. Кроме того, слева и справа на экране формируются заголовки для двух колонок цифр, в которых затем будут отображаться номер ряда (слева) и текущее количество фишек в ряду (справа). Эта информация поможет игроку сообщить программе свой ход. Для размещения информации на нужных участках экрана используется процедура GOTOXY(X,Y), с помощью которой курсор перемещается нужным образом. Параметры X и Y этой процедуры задают новые координаты курсора. Начало координат соответствует точке (1,1) и размещается в левом верхнем углу экрана, поэтому горизонтальная координата увеличивается слева направо, а вертикальная - сверху вниз. Рис.2.4. Вид экрана в начале игры ним 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, поэтому она должна быть глобальной по отношению и к GetPlayerMov
Читайте также: I. МЕТОДЫ, ПОДХОДЫ И ПРОЦЕДУРЫ ДИАГНОСТИКИ И ЛЕЧЕНИЯ Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|