Не рекомендуется принимать данную статью близко к сердцу или как истину в последней инстанции. Скорее как научпоп-грелку для мозгов. Тут довольно много упрощений, упущений или просто странно написанных моментов. Если что-то объяснено совсем криво, то добро пожаловать в комментарии.
Все сказанное далее применимо везде, но детали описаны для защищенного режима x86 (только я опустил префиксEдля регистров) и языков семейства C (в основном C и C++, местами C#). Для понимания рекомендуется знать про указатели и базово представлять, что происходит в процессоре.
Приятного чтения.
Думаю все помнят, что такое функция в математике
Начнем со сложного
Глоссарий
Чувствую, что большинство не имеет ни малейшего представления об указателях
В части про x86 я упоминал о том, что процессор представляет из себя бешенный самоуправляемый калькулятор, которому указом может быть разве что хранилище кода, аппаратный сброс и немаскируемое прерывание. И что у него есть системная шина, состоящая из шины адреса, шины данных и шины управления, к которой подключена память, хранящая данные и код.
А память это огромная одномерная полоска, в которой каждая ячейка имеет свой порядковый номер от 0 до 2^(разрядность шины адреса)-1. И указатель представляет из себя самую обычную численную переменную, хранящую номер (адрес) любой ячейки.
Я не понимаю, почему для многих это настолько сложная тема, а существенное количество новичков забрасывают изучение C после встречи с ними. Они в своей сути максимально элементарны (до тех пор, пока не нужно исправлять уязвимости и ошибки, которые появились в результате злоупотребления или неполного понимания нюансов).
Также существуют ссылки, так или иначе представляющие из себя подвид указателей, но обычно с запретом на арифметику с ними (основная сложность и опасность указателей) и/или подсчетом количества активных ссылок для сборки мусора.
Все языки или имеют указатели (Pascal, C, C++, блоки unsafe в C# и Rust), или являются ссылочными (Java, C#, Python, JS, т.д. (почти все современные языки)). С++ тоже имеет ссылки, но как синтаксический сахар над указателями, призванный упростить их передачу в функции и избавиться от проблемы нулевых указателей.
Ассемблер - если вы даже примерно не знаете, что это такое, то первую часть статьи вероятно можно пролистать и перейти на обсуждение парадигм.
Регистр - именованная численная переменная внутри процессора. Может иметь особое предназначение, а может просто использоваться для хранения любых чисел и арифметики. Их немного.
Инструкция - одиночная команда, представляет из себя последовательность из нескольких байт со специальным значением. Любая программа представляет из себя последовательность инструкций внутри памяти. Указатель на инструкцию, которая будет выполнена следующей, содержится в регистре IP, который сам увеличивается после выполнения каждой инструкции. По-хорошему стоило нарисовать пошаговую наглядную анимацию с демонстрацией всех регистров и куском ассемблерного кода, но мне лень, а в гугле я ничего толкового не нашел.
Метка - константа, содержащая адрес чего-либо. К примеру процедуры или глобальной переменной. Примерно как метка для goto в высокоуровневых языках, но универсальнее. Применяются при написании на ассемблере, в процессоре как таковые не существуют и разрушаются до обычных чисел при ассемблировании и линковке.
Разыменование - операция над указателем, когда тот превращается в переменную с адресом, который был записан в указателе (своеобразный пульт Д/У для переменной).
Парадигма - "стиль" написания и постройки архитектуры программы, частично определяется языком. Технически на том же C можно писать в практически любой парадигме (через костыли можно писать в стиле ООП, через макросы реализовать метапрограммирование, а через нестандартные расширения вообще ядерный бред), но родная для него - процедурная. А Go, к примеру, хоть и имеет недоклассы и методы, но лишен практически всех благ ООП и не сильно далеко ушел от C.
Синтаксический сахар - необязательная возможность, которая сокращает количество кода, повышает его читаемость или удобство поддержки
Стек
Большинство процессоров Фон-Неймановской архитектуры в своей конструкции предлагают механизмы стека. Кто играл в покер должны вспомнить стеки фишек. То есть некие значения, сложенные друг на друга (обычно это переменные, в частности адреса в памяти). При этом основными операциями являются добавление фишки на вершину (инструкция PUSH) и ее снятие (инструкция POP). Еще можно косвенно читать и заменять (перезаписывать) фишки относительно вершины (на вершину указывает регистр SP, неявно обновляется через POP и PUSH) или основания (указывает регистр BP) вглубь.
Стеки можно переключать (но не в MOS6502), это нужно для многозадачности
Помимо хранения локальных переменных стек позволяет делать довольно интересную вещь: мы можем положить на стек все необходимые аргументы (x в математике, но их может быть несколько), адрес следующей инструкции (взяв из указателя инструкции IP, это будет адрес возврата), после чего совершить прыжок на какой-нибудь другой адрес (сохранение адреса и прыжок делаются инструкцией CALL).
А на этом адресе может быть функция. Сначала она кладет текущий BP на стек, после чего приравнивает основание к вершине (BP к SP), тем самым создав для себя "новый стек" сразу после предыдущего (это еще называется стековым кадром), в котором якобы лежит только значение старого основания стека (BP), чтобы можно было восстановить его перед возвратом.
Серое это стековый кадр от предыдущей функции
После чего функция может прочитать переданные ей аргументы относительно основания своего стека вниз (технически это будет выход за границы текущего стека), выполнить с этим какие-либо действия (записать в файл, вывести в консоль, просто перемножить) и сохранить результат (обычно результат сохраняется не на стек, а в регистр AX).
После чего функция восстанавливает старое значение BP, сняв его со стека, и выполняет команду RET, которая снимает со стека адрес возврата и совершает переход на него, тем самым переключившись на инструкцию сразу после CALL.
И эта система так или иначе перекочевала в большинство высокоуровневых языков начиная с FORTRAN. К примеру в C CALL превратился в круглые скобки, RET в return, а адреса и метки в имена (при этом без скобок они все еще являются указателями, то есть адресами).
Пример кода (к сожалению, штатного форматирования не предусмотрено):
#include <stdio.h> int pow2(int x) { return x * x; } int main() { int y = pow2(16); // Вызов функции. В y будет сохранено число 256 printf("%p", pow2); // Без скобок вместо вызова просто выведет адрес функции return 0; // Возврат нуля из главной функции означает отсутствие ошибок. }
Такой стиль программирования называется процедурным (выделение кода в блоки называется структурным). А вот называть процедурный язык функциональным совершенно неправильно, ибо функциональное программирование ≠ процедурное, они даже в разных категориях (императивное и декларативное).
А теперь скомпилируем этот код и разберем ассемблерный листинг
Важно понимать, что стек в большинстве архитектур традиционно растет от больших адресов к меньшим (и в x86). То есть для того, чтобы отодвинуть его вершину вверх, от регистра SP нужно отнимать значения, а вот для ужимания и съедания ненужных значений к указателю на вершину значения прибавляют. И для доступа к значениям относительно основания или вершины это тоже важно учитывать. Это может звучать запутанно, но через время привыкаешь и всё становится очевидным.
Надеюсь это возможно будет разобрать. Код скомпилирован MSVC v19.28 для x86 со стандартными настройками в Godbolt
Post scriptum
Это всё довольно упрощенно. Как минимум, в защищенном режиме используется больше 5 разных соглашений о вызове, которые отличаются деталями реализации. Это было описание для cdecl, обычно используемого в C. Еще часто используются соглашения pascal, fastcall, thiscall, winapi и другие. Fastcall, к примеру, избегает хранения аргументов на стеке, если их возможно передать через регистры, что улучшает производительность. А winapi отличается от cdecl тем, что функция сама очищает стек от аргументов для себя при возврате. А еще я упустил, к примеру, сохранение регистров, которые функция может перезаписать и испортить, а потому обязана предварительно сохранить и перед возвратом восстановить, передачу переменного количества аргументов (как в printf) и возврат значений шире 32 бит (которые не влезут в EAX).
Плюс сейчас мало кто компилирует ПО под защищенный 32-битный режим, а в длинном режиме (AMD64) используется пара других соглашений, основанных на fastcall и имеющих несколько отличий друг от друга.
Так процедура или функция? Или подпрограмма?
Процедурное программирование предлагает делить код на подпрограммы, которые принято называть функциями и процедурами (функция обычно является наиболее понятным, частым и обобщенным названием, поэтому я его использую).
Процедура от функции отличается только тем, что функция возвращает какое-то значение (как в математике), а вот процедура этого не делает. Не во всех языках явно есть процедуры (в Pascal есть, но не в C). В таком случае их заменяют функции, возвращающие ничего (void, Unit, undefined, None).
Хотя и тут есть свои особенности. К примеру функция, возвращающая void в C и Java является прямым аналогом процедур, как-либо использовать возвращенное значение из такой функции невозможно, ибо его нет физически. А вот Unit в Kotlin это синглтон (а-ля единственная и уникальная константа уникального типа), ссылку на который можно присвоить в переменную, но в этом особого смысла нет. Undefined в JS и None в Python тоже уникальные константы специальных типов.
Но не тут-то было
Вроде бы процедура никогда не может ничего вернуть...
Только она этого никогда не делает напрямую. При она этом может записать результат в глобальную переменную, а еще часто принимает в себя указатели или ссылки, по которым может записать результат. Это еще удобно тем, что можно "вернуть" несколько значений. Пример:
void procedure(int x1, int *x2, int *x3) { // Функция ничего не возвращает, то есть это процедура *x2 = x1 * x1; // Разыменовываем указатель и записываем по его адресу результат. *x3 = x1 * x1 * x1; // Разыменовываем другой указатель и записываем по его адресу результат. } int y1, y2; procedure(16, &y1, &y2); // В y1 оказался результат, аналогичный прошлому примеру. А в y2 куб числа.
PS: оператор звездочка при указании типа превращает его в тип-указатель, а при применении на переменную-указатель разыменовывает ее до изначальной переменной. Амперсанд превращает переменную в указатель на нее (иногда еще называется оператором получения адреса).
То есть мы вернули сразу 2 разных значения из процедуры, которая якобы ничего не возвращает. Чудеса. Подобные чудеса есть в том числе в Pascal с явным делением на процедуры и функции (плюс там это сделано немного удобнее). Хотя механизм тут отличается от того, который используется в возврате значения из функции и совпадает с механизмом передачи обычных аргументов, поэтому никакой магии.
Еще про связь с математикой
Главное отличие функций в программировании от функций в математике в том, что они могут делать что-то на стороне и не обязаны возвращать одинаковый результат при одинаковых аргументах.
К примеру функция получения случайного числа по определению не может существовать в математике, если она не принимает в себя предыдущее случайное число или зерно для его видоизменения. Или функция записи в файл, возвращающая 0 в случае успеха и другое число при провале. Ко всему прочему, такая функция имеет побочный нематематический эффект, то есть запись в файл, что тоже недопустимо традиционной математикой без высоких абстракций.
Поэтому придумали чистые функции. По сути это ограничитель, которые делают функцию полным отражением таковой в математике. Им запрещено возвращать разные значения при одинаковых аргументах (точнее запрещено всё, что может такое позволить сделать), запрещено обращаться к нечистым функциям, запрещено обращаться к тому, что не является аргументом или локальной переменной, запрещены вообще любые действия, которые могут сделать что-то на стороне (даже функция sin() в C не всегда является чистой, ибо может зависеть от состояния FPU).
Чистые функции через ключевое слово pure явно есть в D и FORTRAN (проверка на чистоту во время компиляции), а также являются основой функционального программирования.
Чистая процедура тоже имеет право на жизнь, используя механизм со ссылками (на счет указателей не уверен из-за возможности арифметики над ними).
Функциональное программирование
Это очень сложная категория, которую постоянно путают с процедурным программированием. А еще это де-факто противоположный стиль: декларативный. Традиционное императивное программирование детально описывает процесс получения результата, а декларативное сам результат, без деталей реализации (хотя разделение обычно довольно нечеткое). При этом второй типичен для языков разметки типа HTML и CSS. То есть, условно, как одна и та же операция могла бы выглядеть в императивном и декларативном стиле:
document.tags.A.color = "blue" /* Императивный (JSSS). Сделать ссылки синими */
a { color: blue } /* Декларативный (CSS). Ссылки должны быть синими */
Почувствуйте разницу.
И функциональное программирование я никогда не изучал и слишком мало о нем знаю. Так что готовьтесь к ошибкам и не воспринимайте всё за чистую монету.
Внутри чистого функционального программирования
Основано полностью на математике, все функции обязаны быть чистыми. Операция присваивания запрещена (разрешены константы), переменных в привычном виде нет. Прикольно? Очень!
Во многих процедурных языках функции и процедуры являются объектами второго класса (не путать с классами из ООП), что не позволяет их свободно присваивать в переменные, передавать как аргументы в другие функции или возвращать из них (только через указатели). Функциональные языки расценивают функцию как объект первого класса, то есть их можно, а часто нужно передавать в другие функции напрямую.
Это дает некоторые преимущества, особенно в плане безопасности и при работе с многопоточностью (по причине неизменяемости данных и отсутствия глобального состояния), но вся концепция имеет один фатальный недостаток: вы мало чего полезного можете сделать, ибо что ввод, что вывод являются математически нечистыми, а потому запрещены. Вот такое вот гениальное изобретение безумных математиков.
Функциональное и процедурное программирование. Холст, масло
Каждый чисто функциональный язык выкручивается из этого по-своему, к примеру через монады. Это позволяет им существовать вне шуток и даже использоваться на практике.
Это не все особенности функциональных языков, но одни из самых важных. Самый известный такой язык: Haskell. Функциональные F#, Lisp, ML и многие другие не являются 100% чистыми.
Смешанное функциональное программирование
Последнее время часто используются смешанные языки, к примеру вместе с процедурным или объектно-ориентированным программированием, что избавляет от ограничений математики, но дает гибкость в том, что функциями можно оперировать как с любыми другими типами данных, а еще дает много очень удобного сахара вроде замыканий и лямбд. Это C#, Python, JS, частично Java и C++ (в них нужны костыли в виде интерфейсов из одного метода или оберток над указателями).
Особенности
Функции как объект первого класса. К примеру в C# это реализовано через систему делегатов, которые представляют из себя тип-обертку для функций:
Action<string> printer = Console.WriteLine; // Action<string> - тип-делегат. Неявно создаем его объект и присваиваем туда функцию printer("Hello, World!"); // Вызываем функцию через делегат
Локальные функции: как обычные, только вложенные в другую функцию (объявленные внутри нее)
void Func1() { // Глобальная функция void Func2() { // Локальная функция Console.WriteLine("В локальной функции"); } Func2(); }
Лямбды: возможность объявить безымянную функцию посреди кода (часто удобнее локальных)
var pow2AsLambda = x => x * x; // => - оператор лямбда-выражения pow2AsLambda(5); // Вернет 25
Замыкания (можно использовать вместе с лямбдами и локальными функциями):
int someValue = 42; var pow2AsLambda = x => x * x + someValue; // someValue будет захвачено замыканием, хотя напрямую не передано pow2AsLambda(5); // Вернет 67
Особенность замыканий в том, что они могут захватить локальную переменную родительской функции внутрь себя, продлевая ей время жизни за пределы блока с кодом. После чего такую лямбду можно передать в другую функцию, которая просто так не имеет доступа к someValue (в обычных условиях someValue вообще уже будет уничтожен), а вот переданная лямбда сможет ее прочитать или записать всегда и откуда угодно. И самое интересное то, что значение этой переменной будет сохранятся между вызовами к замыканию. То есть она становится глобальной, но видимой только из функции-замыкания.
ООП
Посмотрим на индекс TIOBE по популярности языков. Фиолетовым я пометил чистые функциональные языки. Языки, имеющие возможности, присущие функциональным языкам (смешанные) - синим, процедурным - красным, а объектно-ориентированным (ООП) - зеленым. Языки, в которых нельзя объявить функцию, принадлежащую самой себе или модулю, процедурными считать не совсем корректно (Java).
Не претендую на 100% точность
Видите фиолетовые точки? И я не вижу. А вот синих 12. В то же время красных и зеленых по 16. При этом реально применяемый чисто процедурный язык всего 1: С. Остальные имеют какую-либо встроенную поддержку других парадигм.
И сейчас практически всё большое ПО пишут в ООП. Ибо позволяет хоть как-то сдерживать структурированность кода после перехода с десятков тысяч строк к сотням (и миллионам).
Внутри
ООП является развитием идеи процедурного программирования. Проблема была в том, что помимо кода в программе существуют еще и данные, которые было бы неплохо связать с соответствующим им кодом. И через некоторое время после появления процедур появились структуры (они же записи в Pascal, не путать со структурным программированием).
Структуры были удобным способом объединить несколько переменных в единое целое. Пример:
typedef struct user_struct { char *username; int userid; int reputation; } user_t; user_t someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.reputation += 10; // Обращаемся к полю
(Вместе с этим объявляем структуру новым типом через typedef, что не делается по умолчанию в C, в отличии от C++. Один из ярких примеров несовместимости C и C++):
После чего user_t превращается в новый тип, как int или char. Можно создать полноценную переменную типа user_t (или массив такого типа), а потом обратиться к какой-нибудь ее части (полю) через оператор точки (.). Можно сделать функцию, которая будет принимать или возвращать переменную типа user_t (к примеру someuser). Можно сделать указатель на user_t, тогда для обращение к полю используется оператор стрелки (->). Или можно встроить переменную типа одной структуры в другую, это тоже не запрещено. Внутри структурная переменная представляет из себя последовательно слепленные поля в одно целое.
Если поля разного размера, то часто добавляют пустоты между полей в целях выравнивания по размеру наибольшего поля и улучшения производительности, но это сложная тема
Возвращаемся к ООП
Суть ООП в том, что помимо полей структуры могут содержать процедуры и функции. Такая структура называется классом, переменная класса - объект, а процедура класса - метод.
Метод принципиально ничем не отличается от любой процедуры или функции, кроме того, что неявно принимает в себя специальный аргумент this (в Python это делается явно, в некоторых языках вместо this может быть self). Он содержит в себе ссылку/указатель на объект класса, от которого был вызван. Пример:
class User { public: char *username; int userid; int reputation;
void print() { printf("Пользователь \"%s\" (%i). Репутация: %i\n", this->username, this->userid, this->reputation); // Постоянно писать this-> не обязательно. Если локальной переменной с таким именем нет, то будет произведен доступ к полю } }; User someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.print(); // Вызвали метод, print() неявно получил в себя указатель this, который указывает на someuser someuser.reputation += 10;// Обращаемся к полю someuser.print(); // Теперь вывод поменяется
Да, никто не мешает объявить обычную функцию, которая будет явно принимать в себя указатель на User и ничего не поменяется, и так повсеместно делают в C (тот же WinAPI на этом построен целиком и полностью), но ООП на самом деле крайне сложная, большая и холиварная тема, которая развивалась на протяжении 60 лет в разных направлениях и которую поднимать тут глупо. Только опишу еще несколько разновидностей методов:
Статичный метод - как обычный метод, только не получает в себя this и вызывается от имени класса, а не объекта (не someuser.method(), а User::method()). Зачем это нужно и в чем отличие от просто функции? Статичный метод можно спрятать за инкапсуляцией, плюс он привязан к классу, а не болтается в глобальном пространстве имен.
Конструктор - автоматически вызывается при создании нового объекта. Может иметь аргументы, тогда обязан явно вызываться как функция с именем класса, т.е. User("IvanKr08", 17002, 253)
Деструктор - есть в C++. В C# называется финализатором и имеет несколько важных отличий. Автоматически вызывается перед уничтожением объекта (к примеру выход из области видимости, явное удаление оператором delete или удаление сборщиком мусора в C#). Не может иметь аргументов.
Свойство - есть в Delphi и C#, куда перешел от первого. С точки зрения синтаксиса это поле, которое можно читать и записывать, только вместо прямого обращения в память вызываются соответствующие методы (set и get). Пример: ... int SomeProperty{ get =>42; set => Console.WriteLine("Set"); } ... test.SomeProperty = 10; // Значение никуда не сохранится, будет выведено "Set" в консоль Console.WriteLine(test.SomeProperty); // Всегда будет 42
Перегрузка оператора - специальный метод, который позволяет переопределить стандартные операции для типа. К примеру нельзя сложить два объекта User или User и число, но если перегрузить оператор +, то можно будет определить свою логику для этого (не обязательно, чтобы оно что-то реально складывало. Это может быть любое действие).
Индексатор - синоним перегрузки оператора квадратных скобок. Объект начинает вести себя как массив.
Функтор - перегрузка оператора круглых скобок (иначе называется перегрузкой оператора вызова функции). Позволяет "вызывать" объект так же, как и функцию. Звучит запутанно, но можно погуглить. Есть в C++. Также бывает функтор в функциональном программировании, но он не имеет ничего общего с функтором в C++.
Виртуальная функция (метод) - самый интересный и сложный тип, составляет основу полиморфизма. Вкратце объяснить его невозможно, но одним из ключевых принципов ООП является наследование, то есть один класс (наследник) копирует в себя все поля и методы другого класса (родитель), и может добавить новые. И суть в том, что указатель (ссылка) на объект класса-наследника может быть присвоен в переменную-указатель родительского типа. А самое интересное то, что любые обращения (к примеру вызовы методов), сделанные через родительский указатель, будут вести себя аналогично обращениям, сделанным напрямую через указатель с типом наследника. Это называется полиформизмом. Но это было бы не слишком полезным, если бы не возможность объявить метод в родительском классе виртуальным, а в наследнике полностью переопределить его код.
И то, что я забыл
Послесловие
Если вы осилили эти 3000 слов и даже смогли что-то понять, то найдите и распечатайте себе утешительную грамоту. А я устал и пойду искать смысл жизни...
UPD: исправлены косяки форматирования, очепятки, добавлено еще пару пунктов про разновидности методов и функциональное программирование.
В первой главе мы с вами узнали, как и где создавались первые "шахматные автоматы", которые были всего лишь имитаторами программируемых шахматных роботов. Сегодня знакомство с первыми настоящими шахматными алгоритмами и программами.
*** Глава 2. Как роботы научились играть в шахматы ***
Чтение советской и американской прессы 70-х годов прошлого XX-го века оставляет приятное послевкусие.
Конечно, нельзя было говорить о дружбе между СССР и США, но отношения явно улучшились по сравнению с послевоенными годами.
В американской прессе мелькают сообщения о смягчении давления американской бюрократии на Коммунистическую партию США. В частности, в 1973 году федеральный окружной суд в Аризоне постановил, что большая часть закона против американских коммунистов неконституционна, и Аризона должна допустить КП США к участию в голосовании на всеобщих выборах ("Блавис против Болина").
В советской прессе насмешки над убогим мещанским западным (в основном американским) образом жизни продолжались (в духе Михаила Задорнова "ну, тупые"). Но при этом явно начала изменяться эмоциональная окраска этих насмешек. Злая язвительная сатира потихоньку менялась на добродушный юмор с весёлыми приколами.
Вот мы читаем сообщение о нищем, который обитает на богатой парижской помойке. Ничего особо интересного в этом факте нет. Не было никакого секрета в том, что на этом Западе нищих огромное количество, как блох на бродячей собаке. Но именно в этом нищем была интересная изюминка. На своём плакате, ниже стандартного объявления "подайте плиз, кто сколько может жертве холокоста, бюрократии и бездушия", нищий сделал странную и наглую приписку "доллары США не принимаю".
Не знаю, придумал журналист эту хохму, или реально зафиксировал нечто подобное, но эта короткая заметка порождает у читателей множество мыслей, начиная от надежд, что долларовая долговая пирамида скоро рухнет до желания дать нищему полезный совет: "бери, дурачок, что дают, потом ненужное выбросишь".
Учёные экономисты по обе стороны океана начинают осторожно высказывать идеи о возможности "конвергенции" капитализма и социализма. При этом, в США должны усилиться социальные гарантии для трудящихся, а в СССР для "деловых людей" должны быть предоставлены возможности для полезных экономических частных инициатив (типа строительства личных дач и не только).
Самое главное, что в такой атмосфере о прямом военном конфликте не могло быть и речи. Решались вопросы о возможностях сотрудничества в разных областях.
В 1972 году в Москве председатель Совета министров СССР Алексей Косыгин и президент США Ричард Никсон подписывают "Соглашение о сотрудничестве в исследовании и использовании космического пространства в мирных целях". В 1975 году в рамках этого соглашения был реализован совместный полёт советского и американского пилотируемых космических кораблей со стыковкой на орбите (знаменитый проект "Союз - Аполлон").
Вот в таких условиях мирной конкуренции и делового сотрудничества СССР и США с явным желанием обеих сторон уйти от прямых военных столкновений в 1974 году состоялось грандиозное событие для всех любителей шахмат и прикладного программирования: первый в мире чемпионат мира среди шахматных программ.
Состоялось это мероприятие в Стокгольме во время конгресса ИФИП (IFIP, International Federation for Information Processing).
Лидерами в области шахматного программирования были американцы. В США было 50 действующих шахматных программ, во всем остальном мире (Европа + СССР) около 20. Также в США уже был богатый опыт проведения внутренних чемпионатов. Последний чемпионат США стал отборочным к первому чемпионату мира. Лучшими оказались программы: "Чесс-4.0", "Теч-2", "Хаос" и "Острич". Они и представляли США на этом турнире.
Что же касается нашей страны, то у нас в боевом режиме была единственная программа "Каисса" и ещё несколько программ в стадиях подготовки и перспективной разработки. Именно "Каисса" и представляла СССР на этом турнире. По итогам турнира "Каисса" заняла первое место и завоевала золотую медаль весом 110 грамм.
После окончания турнира "Каисса" в виде бонусного трека сыграла дополнительную товарищескую партию с лучшей американской программой "Чесс-4.0". После долгой и упорной борьбы партия завершилась вничью.
Насколько сильно играли лучшие компьютерные программы в 1974 году? Мне представляется, если бы они играли в сегодняшних (2025 год) турнирах для людей, например, на популярном шахматном сайте "ЛиЧесс", то изначально показывали бы рейтинг в диапазоне 1800-2000 в блице с контролем 5+0. Для сравнения сегодняшние лучшие гроссмейстеры показывают здесь рейтинги 3000+ или, как минимум, около того. А если бы обсчитывали рейтинги современных лучших компьютерных программ типа "Стокфиш" и "АльфаЗирро" при их играх с людьми, то мы увидели бы рейтинги 4000+ или даже 5000+. Короче, эти современные роботы били бы всех людей без малейших шансов для последних. Примечание. При условии взаимной честной игры, но это уже совсем другая тема.
После первого чемпионата разработчики упорно работали над усовершенствованием своих программ.
В 1977 году состоялся второй чемпионат мира среди компьютерных программ в канадском городе Торонто.
Наша "Каисса" приняла участие и в этом чемпионате. Уже в первом туре чемпионка преподнесла неожиданный сюрприз для всех, включая зрителей, своих разработчиков и присутствовавших на турнире гроссмейстеров и мастеров.
Позиция из партии: "Duchess" - "Каисса", Торонто, 1977. Тур: 1.
"Каисса" до этого игравшая неплохо в этой чуть лучшей позиции вдруг делает ряд странных ходов, начиная отсюда: 29. … а5?
30. g4 Фe6
31. Лc6 a4?
32. Ф:а4 Лd6
33. Л:d6 Ф:d6
34. Фа8+
Здесь все ожидали естественного хода 34. … Крg7
Но "Каисса" вдруг ставит под бой ладью 34. … Лe8 и затем постепенно проигрывает без каких-либо шансов.
После партии, когда Каиссу спросили, в чем дело, она объяснила, что ход 34. ... Крg7 гораздо хуже, чем сделанный ею ход 34. ... Лe8.
В доказательство "Каисса" показала такой вариант:
34. ... Крg7 35.Фf8+! Крxf8 36. Сh6+ Сg7 37. Лc8+
"Каисса" демонстрирует вариант, которые не увидели даже гроссмейстеры
37. … Фd8 38. Л:d8+ Лe8 39. Л:e8X.
Специалисты, среди них были гроссмейстеры Ботвинник, Эдуард Ласкер, Ганс Берлинер, канадский международный мастер Леон Пиасетский эту комбинацию не обнаружили и объясняли народу этот заскок Каиссы "несовершенством шахматных программ".
В конечном результате турнира Каисса разделила 2—3 места с программой Duchess. Победила в чемпионате программа "Чесс-4.0".
До сих пор идут диспуты, а увидела бы программа Duchess во время партии этот выигрывающий ход 35.Фf8+
Далеко не факт, учитывая, ограниченность времени на обдумывание в турнирной партии.
В любом случае, если ход 34. ... Лe8 объективно сильнее (т.к. затягивал поражение на много ходов), то никто не будет спорить, что практических шансов больше у скромного хода 34. ... Крg7
Мне стало любопытно, а как бы в критической позиции пошла бы современная программа: 34. ... Лe8 или 34. ... Крg7 - ?
Я задал этот вопрос "Стокфишу" и получил ответ: 34. ... Лe8
Вот так!
Такие тонкие психологические моменты не понимали шахматные программы в 1977-м году, не понимают их они и сейчас, в 2025-м.
Возьмем этот факт на заметку, он нам пригодится для дальнейших рассуждений.
А как вообще роботы научились играть в шахматы? Точнее говоря, кто был их первым учителем?
Вообще говоря, много умнейших людей брались за эту интереснейшую проблему и решали её с разной степенью успеха.
Традиционно считается, что самым первым шахматным программистом в мире был Алан Тьюринг. В 1951 году он написал алгоритм Turochamp, с помощью которого машина могла бы играть в шахматы. Самое забавное, что это был чисто теоретический труд. У Алана не было компьютера, чтобы проверить свою программу на практике.
Тем не менее, его идеи были использованы другими учёными, а идеи тех, в свою очередь, новыми учёными, и вот, что мы имеем в сухом остатке.
Попробуем набросать примерный алгоритм игры в шахматы.
Что нам нужно сделать? Написать программу, которая умеет находить сильнейший ход в любой позиции.
Немного подумав, почитав разных полезных статей, становится ясно, что тут есть 2 принципиальных краеугольных камня.
Функция, которая оценивает позицию (оценочная функция).
Функция, которая как-то умеет определять максимальную глубину просмотра.
Давайте, попробуем набросать функцию, которая оценивает позицию.
Для простоты пока сделаем глубину просмотра равную одному полуходу и анализировать будем начальную позицию.
Примечание. Пусть будет 0.1 балл за одно свободное поле под атакой.
Премия за очередь хода. Пусть пока будет 0. Не уверен, что это вообще хорошая идея.
Проверку на мат проводим. Вообще, вряд ли кому-то будет мат в начальной позиции или после первого полухода. Но проверять надо.
Оценка позиции = Белые - Черные = (38.2+18)-(38.2+18) = 56.2-56.2 = 0
Теперь нам надо подобным образом оценить все позиции после всех возможных ходов.
Оценка позиции после 1-го хода белых
Ход Баллы
a3 0.0
a4 0.2
b3 0.2
b4 0.2
c3 0.2
c4 0.3
d3 0.6
d4 0.7
e3 0.9
e4 0.9
f3 0.0
f4 0.1
g3 0.2
g4 0.2
h3 0.0
h4 0.2
Кa3 0.1
Кc3 0.3
Кf3 0.3
Кh3 0.1
Получается следующий результат. Если применять данную оценочную функцию и использовать глубину расчета на 1 полуход, то в данной позиции получаются лучшие ходы: e3 или е4.
Теперь мы можем уточнить оценку начальной позиции. Если изначально она была равна 0, то после первого полухода стала равной 0.9.
Разумеется, если мы посмотрим чуть глубже (т.е. оценим все позиции после всех возможных 2-х полуходов), то оценка начальной позиции опять изменится. Наверное, она опять станет равной 0, например, после 1. e4 e6.
А если посмотреть немного глубже, хотя бы на 20 полуходов? Все это, конечно, можно сделать, посвятив этому процессу месяц или два, но это уже всё сделано до нас.
Не совсем i8086. Источник: http://ic.onidev.fr/map/AMD_9586A.html
Думаю, что ни для кого не секрет, что подавляющее большинство современных ПК используют архитектуру, которой скоро исполнится 50 лет. Ее современный вариант заметно отличается от того, что было в 1978 году, но при этом сохраняет практически полную двоичную совместимость (современные ПК без особого труда запускают MS-DOS, проблемы начинаются при работе с периферией). Я попытался собрать наиболее ключевые особенности, этапы эволюции и поколения архитектуры.
Вступление
1978 год. Произошло несколько политических революций, сменилось трое римских пап, открыли первый спутник Плутона, многие еще не родились, а Intel выпустили 16-битного наследника i8080: i8086, который в последствии практически полностью вытеснил другие архитектуры из потребительских ПК и стал серьезным шагом к стандартизации.
Рынок ПК на тот момент и еще в ближайший десяток лет был слабо похож на современный. Было много относительно бюджетных машин на MOS6502 (Apple I, Apple II, разные Commodore) и Z80 (ZX Spectrum), к середине/концу 80х начали появляться машины на заметно более совершенном и 16/32-битном Motorola 68K (тоже очень интересная архитектура), но общее у них было ровно одно: абсолютная несовместимость ни с кем и никак. Нет, появлялись +- совместимые между собой серии по типу Amiga или Macintosh, но они были проприетарными, а в конечном итоге загнулись (Amiga перерождалась, но в итоге умерла. Macintosh выжил только благодаря удаче и iMac, в последствии перейдя на x86 на много лет).
Причина: IBM-PC.
Про i8086
Во-первых, крайне краткий и упрощенный экскурс в работу наиболее типового процессора Фон-Неймановской архитектуры.
По факту процессор представляет из себя бешенный калькулятор, который последовательно выполняет различные операции и преобразования над числами, попутно управляя самим собой. Для этого у него есть
Многое поменялось, но суть осталась
1. Шина и память. По сути для процессора это одно и то же. Их можно представить как длинную полоску из нумерованных ячеек, способных хранить число от 0 до 255 (представьте себе швейный сантиметр), что не всегда верно (физически шина и память устроены сложно и разделены на много устройств, но при связи по шине они обычно превращаются именно в одномерную ячеистую полоску). Для чего нужна память понимают, наверное, все.
Вместе с этим там же обычно сидят периферийные устройства, к которым можно обращаться так же, как и к памяти. Пункт диапазонов памяти в диспетчере устройств Windows по сути и отвечает за то, как устройства делят эту шину между собой и памятью.
Шина состоит из шины данных, адреса и управления. Ширина шины данных это одна из ключевых характеристик, которая определяет битность процессора, еще от нее сильно зависит скорость работы с устройствами и памятью. По ней передаются, как ни странно, данные. Шина адреса представляет из себя индекс в полоске ячеек, по которому требуется произвести действие. А шина управления используется, к примеру, для выбора между чтением по адресу и записью.
2. Регистры. Маленькие именованные кусочки памяти внутри процессора. Самое быстрое и легкодоступное, что у него есть (современная разница в скорости по сравнению с памятью примерно как между взять карандаш из ящика (10 секунд) и поехать за ним в магазин (пол часа-час)). Бывают двух ключевых типов: общего назначения и особого (специального) назначения.
Первые обычно имеют размер машинного слова и используются как хранилище операндов для операций. К примеру 2 регистра могут использоваться как слагаемые, после чего в первый будет помещена сумма (особенность многих архитектур в том, что они не могут явно задействовать больше 2 регистров. Частое исключение - FMA). Ко всему прочему, регистры обычно выступают посредником при чтении/записи памяти (опять таки, зависит от архитектуры, но в некоторых вообще запрещены операции напрямую с памятью без предварительной загрузки всего в регистры, другие разрешают только один аргумент для операций брать из памяти). Таких регистров относительно мало, обычно от 4 до 32.
Название вторых крайне общее, ибо все они имеют абсолютно разные предназначения. Чаще всего встречается регистр флагов, в котором каждый бит отвечает или за состояние процессора, или за результат логической операции (для этого обычно есть инструкция CMP, которая вычитает одно число из другого (отбрасывая результат) и заносит в регистр флагов статистику: первое число было больше, меньше, равно, т.д. Потом этим может воспользоваться инструкция условного перехода). Еще есть стековые регистры и регистры, уникальные для архитектуры, но этот экскурс и так слишком длинный.
Самый важный и присутствующий везде регистр: указатель инструкции. Указывает, в какой ячейке памяти находится выполняемая инструкция. Самостоятельно увеличивается после выполнения каждой инструкции, но может быть явно перезаписан инструкцией условного или безусловного перехода на адрес (if-else в языках высокого уровня).
Если вы не закрыли пост, не уснули и за вами не приехала дурка, то продолжаем.
А теперь конкретно про i8086 и x86
20 бит шина адреса, то есть мегабайт ОЗУ, 16 бит шины данных, Фон-Неймановская архитектура, CISC, аппаратные деление и умножение, 4 16-разрядных регистра общего назначения (AX, BX, CX, DX), 8-битные регистры общего назначения, физически совмещенные с 16-битными (AL, AH, BL, BH, т.д. Делят на 2 части 16-битные регистры), 2 индексных (SI, DI. Для строковых операций), 4 сегментных (сегмент кода CS, сегмент стека SS, сегмент данных DS, дополнительный сегмент ES), 16-битный регистр флагов (FLAGS), указатель инструкции (IP). Защиты памяти (MMU) нет, полноценных механизмов многозадачности тоже.
i8088 отличался тем, что имел 8-битную шину данных и технически его можно было назвать 8-битным процессором. Это его замедляло, но зато с ним можно было построить более дешевую систему на старой 8-битной обвязке.
Сегменты
Пропущенная мною часть описания процессора, так как она присуще именно x86 по причине 20-битной шине адреса. Указатель инструкций 16-битный, все операции с памятью тоже 16-битные. Естественно, что 16-битным адресом покрыть все 20 бит адресного пространства было бы как минимум проблемно, как максимум невозможно. Но надмозговые инженеры Intel выход нашли: теперь у нас есть сегменты, а все операции с памятью локальны по отношению к ним. Это создало жуткий геморрой, особенно в высокоуровневых языках (3 типа указателей: ближние, дальние и огромные), но зато облегчило портирование старого ПО с i8080, всё адресное пространство которого влезает в 1 сегмент.
По факту сегмент представляет из себя смещение для логического адреса по отношению к физическому адресу. Значение сегментного регистра умножается на 16 (сдвигается на 4 бита) и прибавляется к логическому адресу для вычисления физического адреса, который будет выдан на шине адреса. Это приводит к тому, что у одного физического адреса появляется 16 логических "синонимов".
Если вы ничего не поняли, то это нормально. Никто не понимает, а потом приходит прозрение (и ночные кошмары). Я не знаю, как это нормально объяснить. У меня есть график, но я не уверен, будет ли он читаем и понятен
Блоки это сегменты, по горизонтали физическое адресное пространство (1 мегабайт), внутри блоков логический 16-битный адрес (64 килобайта, которые можно адресовать внутри сегмента). Вертикаль показывает наложение логических адресов на физические (те самые 16 "синонимов"). При этом в i80286 возможно переполнение и получение доступа к памяти за пределом 1 мегабайта
CISC и RISC
Это легко. CISC предлагает увеличенный набор инструкций взамен на сложность архитектуры и процессора. Время выполнения и длина инструкций может быть совершенно непредсказуемой, иногда встречаются конструкции из высокоуровневых языков (к примеру строковые операции в x86. Подсчет длины строки (strlen()) можно реализовать де-факто одной инструкцией). Удобно для написания на ассемблере, часто не очень удобно для разработчиков компиляторов. Вместе со сложностью растет энергопотребление. Это x86, i8080 и M68K.
RISC же предлагает упрощенный набор инструкций взамен их максимальной оптимизации. Все инструкции должны умещаться в строго одинаковое количество байт. Вместе с этим часто запрещено брать операнды из памяти и увеличено количество регистров. Часто запрещено обращаться к памяти без выравнивания по словам. Иногда даже нет операций деления и умножения, их приходится реализовывать программно. Типовые представители: ARM, RISC-V. MOS6502 можно в некоторой степени назвать RISC, но у него только 1 регистр и один аргумент он всегда берет из памяти (тогда так можно было делать, память была примерно равной по скорости с процессором).
Есть другие варианты, такие как VLIW или шуточные MISC, URISC и ZISC. В дикой природе не встречаются, только если VLIW у "Эльбруса".
А теперь IBM
Сюда хоть кто-то дочитал?
Как гром среди ясного неба начался 1981 год, а IBM представили свой IBM-PC, использовавший i8088. И знаете, получилось хорошо. Проблема была одна: дорого (зачем выкидывать пару зарплату на какой-то электронный гроб?). Но их покупали для бизнеса, покупали просто небедные энтузиасты, причем в больших количествах. Ожидания оправдались в 9 раз.
16-килобайтная версия стоила $1,500 (не забывайте про инфляцию, это около $5000 сейчас). Apple II с 4 килобайтами на момент выхода в 1977 стоил $1,298. Но, конечно, к моменту выхода IBM-PC Apple II успел подешеветь и нарастить память, хотя отставание в производительности было колоссальным. Но простенькие машинки Commodore были многократно дешевле и до, и после.
Amiga вышла сильно позже (1985) и в начале тоже стоила неприятно, но потом подешевела и нашла своих покупателей благодаря отличному звуку и графике. Пока IBM предлагал исключительно пищалку и ядовитый EGA (а то и малиново-голубой CGA) вплоть до 87 года за много тысяч, Amiga уже в 1985 предлагала вот такое (а еще графическую многозадачную ОС), а в 1987 делала это же за $800 в базовой комплектации. Очевидно, что покупал обычный человек себе домой, не искушенный бизнесом и работой в Excel.
А теперь главная ошибка IBM, которая их одновременно и погубила, и сделала IBM-PC стандартом: они не стали закрывать архитектуру за патентами и сделали ее крайне расширяемой. Любой мог прийти и купить за небольшую сумму всю необходимую документацию вплоть до исходников BIOS, после чего начать продавать свои платы расширения или вообще компьютеры целиком, полностью совместимые с другими IBM-PC. Причем делать это стали уже через год и очень активно. Так активно, что IBM обос... профукались и в итоге к 2006 году продали свой компьютерный бизнес от греха подальше.
Но статья у нас о поколениях процессоров, так что мы летим назад в 1982 год...
i80286
Технически существовал i80186 и i80188, но они совершенно неинтересны. Не могу сказать, чтобы и i80286 был сильно интересным.
Первое существенное отличие второго поколения x86: шина адреса теперь 24 бита, то есть 16 мегабайт. А в процессоре появился новый режим: защищенный. При этом режим, в котором работал i8086, стал называться реальным. Все последующие процессоры поддерживают все режимы предыдущих, и при этом всегда запускаются в реальном, даже спустя 50 лет. Помимо этого добавили сотню новых инструкций, в основном для работы с защищенным режимом, и нарастили производительность.
Суть в том, что в реальном режиме был коммунизм: все жили равно и ни у кого не было привилегий ограничений. Но это было крайне опасно, неудобно и мешало созданию полноценных многозадачных ОС, так как любая программа могла залезть в другую и что-нибудь ей поломать, а то и влезть и сломать ОС. Даже не обязательно специально. И на перспективе ограничение на объем ОЗУ начинало переходить из космического в потенциальную проблему недалекого будущего.
Защищенный режим на то и защищенный, что работает в связке с MMU, который позволяет разграничивать регионы памяти под разные программы и привязывать логические адреса к разным физическим адресам, тем самым позволяя реализовать виртуальную память, файлы подкачки и прочее.
Но были у защищенного режима фатальные проблемы. Ключевая: переключаться из реального в защищенный режим было легко, а вот из защищенного в реальный... ну можно было аппаратно сбросить процессор (попросив нажать пользователя на кнопку Reset). Ни о какой одновременной работе защищенного ПО со старым для реального режима речи идти не могло. Потом придумали подвести хитрую схему для программного сброса и вручную сохранять то, что будет при этом сбросе утеряно, но проблему полностью это не решало как минимум из-за серьезных тормозов и отсутствия должных механизмов для "кастрирования" от вредных привычек того, что хочет работать в реальном режиме.
Ничего хорошего из этого выйти не могло, так как всё старое ПО затачивалось под реальный режим с MS-DOS и не могло работать в ОС, использующих защищенный режим. А ОС без программ никому не нужна.
По поводу технической части мне сказать особо нечего, ибо я не работал с этим процессором и практически ничего не знаю о нем. Знаю, что осталась сегментация, но она работала абсолютно иначе и гораздо адекватнее.
I80386
Прошло 3 года. На дворе 1985 год, Intel учли свои ошибки и разработали новый вариант защищенного режима. Это самый интересный и второй по важности режим, который повсеместно использовался вплоть до конца 2000х и продолжает неявно использоваться до сих пор.
Во-первых, теперь процессор стал полностью 32-битным. Поверх старых 16-битных регистров нарастили новые регистры с префиксом 'E'. То есть теперь есть EAX, EBX, EIP, ESP, EBP, EFLAGS и так далее. Но не сегментные регистры, они остались 16-битными. Шина данных и адреса тоже стали 32-битными (шину адреса любят обрезать под лимиты конкретной платформы, но технически ее возможно было сделать 32-битной без модификаций архитектуры. В последствии разработали расширение PAE, что позволило расширить ее свыше 32 линий и 4 гигабайт).
Во-вторых, теперь появился третий (четвертый) режим: виртуальный 8086.Он совмещал в себе особенности работы реального режима и защищенность защищенного, позволяя достаточно эффективно переключаться между ними, а еще реализовывать одновременную работу множества программ реального режима внутри одной ОС одновременно с защищенными. При этом подобный псевдореальный режим оставался достаточно безопасным, так как многие опасные наглости эмулировались и не допускались напрямую к железу, а память была изолирована.
Продолжение следует
К сожалению, мысль о написании подобного текста у меня возникла слишком поздно, а на часах 4 часа 5 часов утра и я уже физически не в состоянии продолжать писать этот пост. Если это будет интересно, то я напишу продолжение, в котором полноценно расскажу про защищенный режим, костыли реального режима и пропущенный промежуток до появления длинного режима (i80486, Pentium). И так страшно представить, сколько неточностей и откровенно грубых ошибок я тут понаписывал на автомате. Если вы их нашли - просьба указать в комментариях
Обещал не делить посты на куски и такой облом, извините
Приглашаю вас окунуться в удивительный и прекрасный мир шахматного программирования. Сначала, в первой главе, чисто для разогрева, мы, с доброй улыбкой, вспомним не такое уж далёкое прошлое, когда люди пытались имитировать работу шахматных программ. Гримасы истории! Сначала игру людей выдавали за игру шахматных автоматов, сегодня игру компьютерных программ выдают за игру людей. Во второй главе вас ждёт самое начало настоящей истории шахматного программирования, включая эпические битвы советских и американских программ. В третьей главе обзор современного состояния дел на поле шахматного боя. В четвёртой заключительной главе я приглашаю посмотреть мою шахматную партию с одним из лучших на сегодня шахматных ботов. Я вам немного завидую, уважаемый читатель. Вас ждут удивительные и интересные приключения. Добро пожаловать!
В 1777 году в популярной Берлинской газете "Berliner Unsinn und Anderes" было опубликовано необычное объявление.
"Для нового популярного проекта требуется маленький смышлёный мальчик примерно десяти лет. Работа лёгкая, лежачая. Мальчик должен быть спокоен, молчалив и хорошо знать математику. Желательно еврейской национальности. Оплата по договоренности. Бесплатное питание гарантируется в любом случае".
- Марта, дорогая! Мы спасены! - закричал Ганс Липке своей жене, прочитав это объявление, - один из наших десяти детей теперь не будет голодать! Иди сюда!
Жена Ганса Марта немедленно бросила стирку белья, вытерла руки о подол и надела очки.
- Ну и каким жуликам ты поверил на этот раз? - спросила Марта, начиная чтение, - где читать? Вот это что ли, "новый способ быстро разбогатеть?"
- Нет, ниже, там, где про "лежачего мальчика". Наверняка, это какой-то пансионат для особо одарённых детей евреев типа нашего Питера.
- А разве наш Питер еврей? - удивилась Марта.
- Тебе видней, ты ведь его рожала. Да это и неважно! Скажем, что еврей, как они узнают, так это или нет. Не будут же они измерять штангенциркулем его череп. Вообще, Питер по всем параметрам подходит. Маленький, постоянно лежит и молчит.
- А разве он знает математику?
- Сейчас проверим. Эй, Питер! Сколько будет дважды два?
- Четыре, - ответил Питер после минутного размышления.
- Гений! Вставай и собирайся. Пойдём на работу. Какая тебе разница, где лежать и молчать? А там тебя за это будут кормить.
Работодатель, некий Вондерганг фон Шахритер объяснил суть работы.
Имеется ящик из дорогого дерева. На этом ящике расположена шахматная доска с фигурами. Мальчик должен быть размещён лёжа внутрь ящика прямо под шахматную доску. Специальный механизм позволял мальчику чётко видеть движения фигур, а также передвигать их.
- Очень перспективная тема сейчас в Германии, да и вообще в Европе, - с энтузиазмом рассказывал Шахритер, - интерес народа к автоматизации шахматной игры просто зашкаливает. Несколько десятков шахматных автоматов ползают по нашей стране, огромные деньги заколачивают. Да, чуть не забыл. Ты, паренёк, хорошо в шахматы играешь?
Питер впал в глубокое раздумье.
- Задам вопрос попроще, - понимающе кивнул головой Шахритер, - ты в шахматы играть умеешь?
На этот вопрос Питер ответил довольно быстро:
- Не знаю, не пробовал.
- Не слушайте его! - закричал Ганс Липке, - он очень талантливый. Задачки по математике щёлкает как орехи. А любой математик - отличный шахматист, это всем известно.
- Ладно, - махнул рукой Шахритер, - я его научу. Шахматы - это просто. Вот бином Ньютона - это сложно. Питер, ты сам-то хочешь этим заниматься? Будешь работать шахматным автоматом?
- Буду, - ответил Питер неуверенно, но зато быстро.
- Попробовал бы ты только отказаться, - проворчал Ганс Липке, - я бы тебе сделал ход конём по голове, чтобы лучше соображал, чего ты хочешь делать, и чего ты будешь делать.
Так началась карьера Питера, одного из многих шахматных автоматов конца 18-го, начала 19-го веков. Несмотря на то, что "шахматы - это просто" по утверждению Шахритера и упорные тренировки, играл в шахматы Питер плохо. Зато он проявил себя в роли артиста. Когда он проигрывал, он начинал очень правдоподобно рыдать басом. Когда выигрывал (случилось и такое), очень язвительно обращался к сопернику, "вам матец, херр Миттельбрахенветтер, ха-ха-ха".
Народу нравились такие шоу. Нравилась даже относительно слабая игра Питера. Ведь у него мог выиграть даже слабый игрок, а это положительно сказывалось на интересе к автомату Питера. Какой интерес играть с автоматом, который всё время выигрывает?
Шахритер придумал хорошую систему ведения бизнеса. Партия игралась на довольно большую ставку, а также продавались недорогие билеты на просмотр игры. В результате, даже если Питер проигрывал, убыток с лихвой покрывался за счёт зрителей.
К тому же через пару лет, благодаря приобретённому опыту, качество игры Питера заметно улучшилось, что положительно сказывалось на доходах.
Беда подошла с другой стороны. Питер подрос и перестал помещаться в ящик. Можно, конечно, было заменить ящик на другой, больший по размеру.
Но хитрый Шахритер подошёл к решению проблемы с другой стороны. Он просто нашёл другого мальчика, более молодого, подходящего по габаритам. Питер перешёл на должность тренера, консультанта и ответственного за безопасность.
Так дело двигалось и дальше. Постепенно команда Шахритера увеличилась до десяти человек, работа находилась для всех. Для самых способных парней всё-таки делали новые ящики для игры. Другие работали на должностях "подай - поднеси". Ну, чисто как "Ласковый Май"!
Самым крупным успехом корпорации была партия одного из автоматов против самого Наполеона. В шахматы Наполеон играл плохо, но у автомата хватило ума эту партию проиграть. Это было правильное решение. Наполеон очень нервничал, когда проигрывал. В приступе ярости он вполне мог разгромить автомат и поколотить членов команды, тусовавшихся рядом.
Неплохо шли дела у Шахритера и его команды! Но только некоторое время.
Всё хорошее когда-нибудь кончается. Начались разоблачения конкурирующих автоматов, а через это у народа начал пропадать интерес к шахматной игре всех остальных профессионалов рынка. Народ желал играть именно с автоматами, а не с какими-то малогабаритными жуликами спрятанными внутрь.
Доходы сначала упали, а затем и вообще пропали.
Шахритер уехал в Америку в поисках новых идей, а участники его команды разошлись кто куда.
Вот такие гримасы истории! Если сейчас подозреваются люди в том, что они используют подсказки шахматных движков, то тогда подозревались автоматы в том, что за них думают люди.
Прошла эпоха шахматных автоматов со встроенными в них живыми людьми.
Началась эпоха настоящих шахматных автоматов, которые играют вполне самостоятельно.
У проекта появилась чёткая архитектура управления сущностями. Теперь NPC, игроки, питомцы и другие объекты взаимодействуют в мире через систему менеджеров — рассказываю, как это работает.
В игровом мире есть разные типы сущностей: NPC, игроки (Player), питомцы (Pet) и другие. Каждая из них имеет свои состояния (движение, атака, бездействие) и требует управления.
Основные сущности и их поведение
• NPC – управляет собой (перемещение, атака, idle).
• Player – управляется игроком (те же состояния: ходьба, атака и т. д.).
• Pet – похож на NPC, но принадлежит игроку.
Менеджеры и их задачи
1. NpcManager – создаёт NPC, реагирует в случае смерти NPC.
2. PlayersManager – отвечает за вход игроков в мир.
3. PetsManager – управляет питомцами (аналогично NPC, но с привязкой к игроку).
4. EntitiesManager – главный координатор:
o Управляет NpcManager и PlayersManager.
o Обрабатывает взаимодействия (например, если игрок подошёл к NPC, оба получают информацию друг о друге).
5. VisibilityManager– отвечает за видимость объектов:
o Определяет, кто кого видит.
o Периодически обновляет списки видимости для оптимизации.
6. MovingManager – обновляет позиции всех подвижных объектов в мире.
Никогда не говорите это слово при программистах-профессионалах. Вы можете их очень расстроить, кто-то даже может начать плакать.
Это слово — legacy. Legacy это старый код, который написан давно, часто плохо, с использованием устаревших концепций. Работать с ним тяжело, кому-то даже больно. Но отказаться от этого кода часто нельзя, так как переписать всё заново это переписать кучу кода, который писался 5, 10, 20 лет. Слишком дорого
Помните как мы бегали по горам и не придавали значение тому как быстро спускались или поднимались на них?
Как мы знаем в реальной жизни перемещаясь на плоскости горизонтальная скорость у нас постоянная.
Как только мы начинаем преодолевать горы и другие неровности то горизонтальная скорость у нас будет меньше.
Но не в мире Lineage 2 где горизонтальная скорость всегда постоянная и нее зависит от неровностей.
Связанно это с тем чтобы было проще синхронизировать персонажа на сервере и клиенте.
Ведь на сервере нет точной модели мира, а лишь примерное очертание называемое geodata.
А из-за того, что geodata приблизительно повторяет ландшафт клиента то было бы невозможно синхронизировать персонажа по Z оси.
Поэтому синхронизация идет только по X и Y оси.
Видео:
1) Горизонтальная скорость на плоскости постоянная.
2) Как было бы в жизни. Взбираясь на гору горизонтальная скорость падает.
3) Как сделано в игре. Горизонтальная скорость постоянная.
4) Демонстрация из игры. Бежит словно нет никаких гор.
Я разрабатываю эмулятор сервера для Lineage 2 Chronicle 1: Harbingers of war на Node.js.
Столкнулся с проблемой синхронизации скорости персонажа на сервере с клиентом. Когда в игре вы нажимаете мышкой в то место, куда хотите перейти то происходит плавный переход с анимацией движения. На сервере в этот момент тоже происходит движение по таймеру, но не такое плавное.
GIF
GIF
C(client) – двигается плавно из одной точки в другую. S(server) – делает прирост координат по таймеру.
Для примера я взял сборку написанную на java l2j-lisvus Сборок много. Но все они являются fork’ами проекта l2jserver https://l2jserver.com/И многое наследуется. В том числе и передвижение персонажа.
В l2j-lisvus, как и во всех сборках l2jserver перемещение персонажа на сервере идет при помощи таймера с приростом одинаковых значений.
Проблема проявляется, когда нам надо сделать какое-то действие после того, как персонаж добежал до пункта назначения. Например, нанести удар по NPC.
GIF
На коротких расстояниях проблема незаметна. Нога наступает точно в монету.
GIF
На длинных расстояниях действие атака начинается раньше, чем персонаж добегает до цели.
GIF
А если выкрутить скорость на максимум (900) то проблема расхождения очевидна. Это связанно с тем, что помимо скорости бега есть скорость ходьбы.
Как работает передвижение персонажа на сервере.
За основу взяты базовые характеристики персонажа. Скорость бега 126.
126 — это количество внутренних unit’ов за секунду.
На данной схеме идет прирост координат персонажа каждые 1000мс на 126 unit’ов. Исходя из схемы выше пример кода для действий персонажем после достижения пункта назначения:
// Прироста координат нет. Просто считаем когда персонаж дойдет до конечных координат. const distance = 1500; const playerSpeed = 126; const ticks = distance / playerSpeed; // 11.90 const time = ticks * 1000; // 11900mc
setTimeout(() => { // действие персонажа после бега }, time);
GIF
На коротких расстояниях.
GIF
На длинных расстояниях.
Расхождения на коротких расстояниях.
Расхождения на длинных расстояниях.
Зеленой зоной показана точка куда должна ступить нога персонажа если бы не было расхождений.
Рост скорости при развитии персонажа.
126 — это базовая скорость. И по мере развития персонажа будет расти и скорость передвижения. А значит расхождение будет больше. Но перед тем, как создать формулу надо подтвердить теорию, что скорость ходьбы влияет на расхождение.
Данные о характеристиках персонажа передаются от сервера к клиенту.
Выставляю значения walkSpeed: 126. Если скорость ходьбы будет равна скорости бега, то расхождения должны пропасть.
GIF
Нога персонажа достигает правильной конечной точки.
Персонаж синхронизирован и начинает атаку вовремя. Теперь надо понять, как скорость ходьбы влияет на расхождения между клиентом и сервером.
Сколько же персонаж успевает пройти перед тем, как начинает бежать?
Надо поймать момент когда ходьба переходит в бег. Для этого передадим в клиент данные, где скорость ходьбы будет больше скорости бега. Из-за этой разницы будет виден переход и можно будет рассчитать пройденное расстояние при ходьбе.
runSpeed: 10
walkSpeed: 600
GIF
Ходьба быстрее бега.
При скорости шага в 600 персонаж успевает пройти 250, прежде чем начинает бежать.
600 / 250 = 2.4
700 / 291 = 2.4
800 / 333 = 2.4
Из этого вывод, что персонаж перед тем, как начать бежать успевает пройти расстояние в 2.4 раза меньше, чем его скорость ходьбы.
Значит при скорости ходьбы 88 персонаж пройдет 36 unit’ов.
88 / 2.4 = 36
Первое деление — это начало движения (ходьба) а следующие деления — это бег.
Проводить оценку производительности при реальной нагрузке в продуктивном контуре, а не на тестовом стенде.
Не иметь метрик производительности информационной системы, оценивать производительность по обратной связи пользователей — «ой стало медленно работать».
Не проводить тестирование последствий изменений инфраструктуры на тестовом стенде, сразу проводить изменения в продуктивном контуре.
Воспринимать СУБД как черный ящик для хранения данных.
Не привлекать DBA к процессу дизайна и разработки.
[ Использовать ORM для взаимодействия с СУБД.] : по итогам дискуссии в комментариях - пункт исключён из списка особенностей.
Не анализировать коды ошибок СУБД в backend.
Не проводить нагрузочное тестирование.
Пытаться решать проблемы деградации производительности информационной системы путем подбора магической комбинации конфигурационных параметров СУБД и увеличением ресурсов серверов.
Запустить десяток SELECT ... FOR UPDATE и удивляться - почему всё тормозит.
Семь бед - один ответ. Ставь костыль изобрети велосипед.
Всем доброго времени! Наткнулся я не так давно на вот такое изображение:
И решил порассуждать не тему того, а плоха ли такая ситуация и как вообще бывает, как идет разработка backend и frontend в вещах которыми я занимался, возможно кому то будет интересно, а кому то захочется подискутировать со мной)
Во первых о понятиях, что такое "Бекенд" и "Фронтенд", я работал с разными разработчиками и даже студиями и понял что иногда эти понятия отличаются у разных людей (особенно когда речь идет о том как HR их понимает), по этому поясню что я в них вкладываю:
Frontend - "передний конец", это весь визуал который видит человек пользуясь программным обеспечением, а так же скрипты которые исполняются на стороне (на устройстве) пользователя.
на примере сайта, это его верстка, а так же javascript который перелистывает слайды, раскрывает выплывающие окна и т.д.
Backend - "задний конец", это всё что исполняется "под капотом" программы, в случае сайта это на сервере, в случае какой то настольной программы, внутрипрограмные механизмы, например обращающиеся к каким то функциям операционной системы и производя какие то расчеты и действия
на примере сайта, это его серверная часть, механизмы которые к примеру работают с базой данных, получают из неё пост для страницы на которой находится пользователь, и к примеру комментарии для этого поста и т.д.
А теперь как раз о том, что разрабатывается первее и почему.
Возьмём вебсайт, вы например хотите что бы вам сделали интернет магазин, обычно этапы разработки выглядят так:
- Создание и утверждение дизайна
- Верстка дизайна
- Подключение к верстке т.н. "Движка" или cms или "системы управления контентом", или же что очень редко и индивидуально, написания этой самой системы с нуля в порядке индивидуальной разработки и внедрения её в вёрстку.
т.е. по сути тут выходит что разработка Бекенда идёт после Фронтенда - и получается что как будто то что показано в меме это естественный ход событий и ни каких вопросов это не может вызывать?
Стоит сказать что последовательность работ которую я описал, это обычно усреденная разработка, где люди не сильно запаиваются составлением подробных ТЗ. По ней выходит то что когда готов дизайн, и на основе него вёрстка, это всё передается программисту работающему над Бэекендом, и он сразу видит, что и где ему нужно будет подключать и в каком виде: "Здесь у нас поиск, с возможностью сортировки и выбору диапазона дат постов", "Тут мы выводим чат, который обновляется каждые пару секунд" и т.д. Программист сразу визуально видит какие данные в какие элементы интерфейса ему нужно выводить, от чего ему более понятен план работ, по тому что он "визуален".
Но предположим что сроки разработки сжаты, и требуется что бы разработка обоих частей проходила одновременно, в таком случае необходимо очень точное техническое задание, где будет подробно прописано то как будет работать бэекенд, какова будет структура базы данных, какие данные при каких обстоятельствах будут из неё извлекаться, каким образом обрабатываться, в каком виде перезаписываться обратно и в какой части условного ещё не созданного даже в виде дизайна интерфейса отображаться.
Это довольно фантастический план, так как обычно не бывает заказчиков которые чётко знают как должен выглядеть их сервис\сайт\по и даже не всегда представляют себе точный его функционал.
Это всё зачастую формируется как раз во время разработки и утверждения дизайна, и по этому программист который с самого старта одновременно с дизайном начал разрабатывать свой бекенд, чаще всего обречен по ходу появления дизайна и вёрстки вносить коррективы в свою работу, а иногда даже переписывать её часть.
Но есть например аспекты которые можно на мой взгляд проработать и без дизайна, например структуру базы данных и её таблиц, создать какие то общие функции, провести базовую настройку движка или фреймфорка с которым будет работа.
Но всё же когда дизайн утверждён и верстка окончена и принята заказчиком, это уже гарантирует стабильность разработки. (при условии конечно если заказчику не взбредёт в голову что то на ходу изменять, но такие вещи должны быть учтены в договоре и проходить за дополнительную оплату)
Примерно то же самое происходит и при разработке приложения для телефона.
Программисту всегда проще подключать бекенд к чему то готовому, чем прорабатывать так называемые API (простым языком объясняя это точки доступа через которые интерфейс получает данные из бекенда) почти в слепую или по скупому ТЗ (я просто ещё не видел по настоящему подробных ТЗ за исключением тех которые в своей педантично душной манере составлял).
По этому становится очевидно что это вполне себе нормальная ситуация, когда Фронтенд давно готов, на него можно посмотреть, потыкать, а Бекенд ещё только подключается, а то ещё и вовсе в стадии разработки.
Но есть один сценарий когда бекенд идёт первее фронтенда - это когда разрабатывается какое то техническое программное обеспечение у которого изначально вовсе нет интерфейса, например какой нибудь локальный сервер - пишется собственно сама програмина, а её настройка и в целом взаимодействие с ней происходит через терминал\консоль\командную строку.
В данном случае интерфейс появляется уже после того как ПО было разработано, зачастую таким программным обеспечением можно пользоваться и без интерфейса через всё тот же терминал, отправляя команды которые ты вводишь в него вручную (прям как хакер из кинофильма), а графический интерфейс создаётся уже потом(иногда очень сильно потом) просто для удобства пользователя и отображает все стандартные и часто используемые опции (для более тонких настроек за частую используется всё равно командная строка) и по сути графический интерфейс, отправляет те же самые команды что вы бы вводили в терминал, просто делает это по нажатию на кнопочки.
На этом всё, надеюсь кому то было интересно почитать эти возможно сумбурные рассуждения, сейчас попробую выловить все грамматические ошибки в этом опусе, перед тем как нажать кнопку "опубликовать пост"😅
Как известно,
для программирования было придумано много языков высокого уровня (ЯВУ, не путать с Java😏). Но есть ЯВУ,
созданный специально для обучения программированию.
Это Pascal (Паскаль). "Потомком" стал Delphi или, грубо говоря, визуальный Pascal.
Я уже писал, что изучал Бейсик в школьное время https://vombat.su/post/42636-ya-napisal-svoi-fotoshop
. А вот Паскаль изучал уже в студенческие, безответственные, годы.
Turbo Pascal по книгам Фроловых (вроде правильно фамилию помню).
Далее,
жизнь позволила начать работать и, первым делом, начал искать
бесплатную альтернативу Borland Delphi для рабочих целей. Попался
Lazarus IDE со своей версией Pascal, которая ничем не уступала
официальной.
Наклепал
несколько прог в нём. И до сих пор тягаю этот легаси. Просто лень
переписать на что-то более стоящее. Работает и ладно. Даже глюков и
багов не замечено. Мне нраиться. 😎😊
Говорят, что ни в коем случае нельзя озвучивать свои планы, надо держать их в секрете. А то господь Бог обязательно над этими планами посмеется и устроит какую-нибудь каверзу на пути к успеху. Я, однако, считаю, что это пустые суеверия. Бог не будет опускаться до такой мелкой работы, как строить препятствия обычному человеку. Впрочем, даже если и так, не буду упускать интересную возможность проверить эту версию на практике.
У шахматистов есть такое золотое правило: "лучше играть по плохому плану, чем вообще без плана". Почему бы не попробовать применить это правило к писательской деятельности?
Сказано - сделано. И вот перед вами примерный план моих творческих писательских работ на новый 2025 год. В течение года буду иногда в него поглядывать, а в конце года будет интересно сверить прекрасные мечты и суровую реальность.
*** Шахматы ***
Вот с шахмат, пожалуй, и начну набрасывать свои тезисы. Играть буду мало. В 2025 году хотелось бы сыграть всего лишь несколько партий, но сыграть так, чтобы они получились красивыми, и эта красота была доступна для широкого круга любителей. А потом сделать подробные обзоры в текстовом и видео форматах. Может, получится сыграть в каком-нибудь новогоднем турнире уже в начале года? И создать "жемчужину шахматного искусства"? Это было бы неплохо. Но торопиться не буду. Если не получится в ближайшие новогодние каникулы, то займусь шахматной игрой весной или летом. Как будет время и настроение.
Интересно было бы связаться с какой-нибудь шахматной стримершей и провести небольшой тематический шахматный матч. Примерно партий из десяти с контролем 5+0 или около того. Потом можно будет сделать обзор наиболее интересной партии, возможно, это будет интересно и поучительно.
Ранее я уже написал "Учебник шахматной игры для новичков". Он предназначен для тех, кто совсем не умеет играть в шахматы, но хотел бы научиться. Теперь у меня появилась идея сделать учебник шахмат для тех, кто уже умеет играть. Это будет учебник нового типа, его главной задачей будет сделать обучающегося умнее в общем смысле, а не только в плане улучшения навыков шахматной игры как таковой.
*** Компьютерные технологии ***
Я уже сделал ранее пару небольших бесплатных курсов для новичков: "Электронные таблицы Excel" и "Редактор текстов Word". Есть также небольшой урок для новичков с практическими примерами на JS.
В новом 2025 году планирую написать серьезный учебник для JS с отображением текущих современных реалий и попыткой предсказать, что ждет этот самый популярный язык программирования в будущем.
Что же касается серверных технологий, то планирую глубоко разобрать какой-нибудь популярный фреймворк на PHP с архитектурным шаблоном MVC (Model-View-Controller, Модель-Представление-Контроллер). Вероятно, это будет Laravel или Yii2. Хочу сделать эту новую книгу так, чтобы новичку было интересно погружаться в эту тему.
*** Дистанционная работа ***
Вообще, мой девиз для творчества будет такой: "больше произведений полезных, меньше развлекательных". Конечно, в идеале хотелось бы как-то совмещать пользу и развлечение, но, увы, во многих случаях это неподъемная задача.
Планирую написать несколько статей или книг с отображением текущих реалий взаимодействия работодателя и человека, ищущего работу. Конечно, тут под "работой" в основном следует понимать банальную прикладную разработку (программирование). Что испытал на себе, о том и буду вас информировать. Но я постараюсь также сделать некоторые общие выводы для всех профессий методом индукции (от частных случаев к общим выводам).
Думаю, что информация в этих статьях или книгах будет полезна для широкого круга читателей, слушателей, зрителей.
*** Мошенничество ***
О мошенниках я немного писал ранее, вы можете прочитать или перечитать такие опусы: "Мошенники: вчера, сегодня, завтра", "Развод скотины (новогодняя спектакля)", "Финансовый Централ".
В новом 2025 году тема также будет актуальной. Мошенники очень хитрые и коварные, все время придумывают новые схемы обмана. Обращаю внимание на пересечение тем "Дистанционная работа" и "Мошенничество". Одним из важных признаков, что "работодатель" является мошенником - отсутствие профессиональных требований к соискателю. Один из вариантов такой "работы" - работа дроппером. Схема простая. На банковскую карту нового работника поступают денежные средства из разных источников. Обычно, это обманутые физические лица. Далее "работник" перечисляет деньги "работодателю", часто через криптовалюту. Процент от таких операций и будет "заработной платой" нового доверчивого "сотрудника".
Осторожно! Это чистая уголовщина и откровенная подстава!
Ищите работодателей только по рекомендациям надежных знакомых или на солидных порталах поиска работы. Солидные порталы не работают с явным криминалом.
*** Фантастика спринт ***
Я люблю писать в стиле "фантастика на коротком поводке". Смысл такой. В журналистском стиле подробно описываются события, которые произойдут в ближайшем будущем (через месяц, год или два). Потом, когда подходит назначенный срок, интересно сравнить, насколько точно удались предсказания.
Вспоминаются такие истории.
"Русский разведчик в Киеве". События происходят летом 2023 года, а их подробное изложение сделано заранее, весной 2023 года.
"Внедряем робота президентом США 2024". Президента по факту выбрали в конце 2024 года. Но я весь процесс описал заранее, в начале 2024 года.
Планирую в таком стиле написать еще 1 или 2 опуса.
*** Человек или робот? ***
Проблема, которая "заострится" в ближайшем будущем, актуальна уже сегодня. Как отличить человека от робота? Например, вот этот текст, кто написал? Человек? А может нейросеть по запросу типа "Творческие планы писателя на 2025 год"?
Но это ладно. Как бы там не было, авторские права полностью мои. А представим себе ближайшее будущее, когда по городу шляются клоны людей, ПАКи (Программно-Аппаратные Комплексы). На вид этих клонов отличить от людей невозможно. У них живая мимика, они уверенно передвигаются пешком и управляют автомобилем. Они умеют логично и естественно отвечать на вопросы, а также вполне уместно эти вопросы задавать. Самые хитрые из них каким-то образом обзавелись человеческими паспортами. Проблему осложняет то, что некоторые клоны искренне считают себя людьми. А некоторые люди сошли с ума и считают себя роботами.
Единственное отличие человека от робота заключается в том, что у человека есть душа. А у робота ее нет. Но как определить, есть у конкретной сущности душа или нет? Хороший вопрос! И у меня есть на него четкий и однозначный ответ. Никак!
Интересное произведение по этой теме : "Год 2124. Убитый обвиняет". Тут фантастика на стайерскую дистанцию. Дано описание событий, которые произойдут через сто лет 22.07.2124, а описание сделано 22.07.2024. Чтобы узнать, насколько точно эти события описаны, осталось подождать девяносто девять с половиной лет.
*** Василий Пак и Ян Янов ***
Есть у меня парочка брендовых героев.
Василий Иванович Пак - этот тип живет уже сегодня. Шахматный бот в человеческой шкуре. Вполне нормально функционирует в автономном режиме, не вызывая особых подозрений у людей с которыми сталкивается. Себя называет иронически "нейросеть на двух ногах". Участник проектов "Первенство ветеранов России по шахматам" и "выборы президента США 2024". Практически не отличим от настоящего человека. Можно даже сказать, что он похож на настоящего человека больше, чем настоящий человек. Ибо, очень хитер и умеет мимикрировать во всех смыслах.
Ян Янович Янов - "космический волк", абсолютный рекордсмен по количеству дальних путешествий и по намотанным световым годам. В настоящее время пока еще не родился. С нетерпением ждем. В четверг 10.06.2224 года в музее "Космической Славы" будет лично проводить экскурсию и рассказывать о своих космических подвигах. Приходите, если будет время и желание. Будет интересно.
По обеим персонам планирую в 2025 году накатать по одной новой истории.
*** Соблазнение "бояркой" ***
Успешные в коммерческом плане писатели искренне советуют мне не заниматься всякой фигней, которую я описал выше, а заняться настоящим, серьезным делом, а именно, написать "боярку".
Что это такое? Я сам толком не знаю, а из прочитанных мною текстов, я понимаю так, что это такой модный литературный жанр. Действие происходит обычно в России (но не обязательно), при феодальном режиме (вероятно). Главный герой обладает волшебными свойствами (вот это обязательно), является хамом и циником, беспощадно убивая врагов разными способами. Но в душе он является добрейшим человеком, спасая по ходу произведения множество котиков и женщин.
Почему бы не попробовать?
На пробу я накатал несколько строк вот в таком духе.
- Кто такой и чьих будешь?
- Я - слуга князя Святослава, у меня послание для тебя, - ответил всадник и протянул кагану берестяную грамоту.
Каган быстро прочитал короткий текст: "Дорогой мой милый еврейский друг, каган лучшего в мире Хазарского каганата. Иду на Вы! С наилучшими пожеланиями, искренне Ваш князь Святослав". Каган раздраженно бросил грамоту в огонь и задумчиво молвил:
- Твой князь с катушек съехал. Ему пора обратиться к психотерапевту. Сколько он таких грамот послал в этом году?
- Эта десятая! - гордо заявил посланник, - статистика хорошая. Девять побед и ни одного поражения!
- А какая статистика по почтальонам? - поинтересовался каган.
- Тут сложнее, - грустно вздохнул посланник, - повесили троих, троих утопили. Двоих женили на своих женщинах. И только одному удалось спастись. Отпустили его. Пожалели. Кстати, это был я. Теперь дрожу и надеюсь, может, опять повезет?
- Увы! Не могу тебя обнадежить, - развел руками каган, - снаряд два раза в одну воронку не попадает.
Ну и далее все в таком духе.
Вам понравилось? Сообщайте. Если вам понравился этот кусок, то накатаю на пробу полное произведение килобайт на двести или даже на триста.
Но все эти "боярки", "псевдо-боярки", пародии на "боярки" по остаточному принципу. Если вдруг в голову ударит изнутри какое-нибудь вещество или снаружи на голову упадет какой-нибудь кирпич. Короче, по ситуации.
Вот и все мои творческие планы на 2025 год.
А у вас какие планы? Сообщайте, очень любопытно узнать.
Вот говорят, что чтобы научиться программированию, надо программировать. И не задачи решать, а именно проекты какие-то реальные. А как такое начать делать, когда не понимаешь ещё ничего?
Да очень просто. Делайте прототипы реальных проектов. Вот банкомат. Ну возьмите и сделайте программу, которая пишет, что у вас на счету 5000, и спрашивает, сколько снять. Потом пишет, сколько сняли и сколько осталось. Вполне себе банкомат. Даже конструкцию if... знать не надо.
А потом можно, с получением знаний уже брать создавать более продвинутый прототип. Добавить другие варианты действий, сохранение информации, чтобы операции предыдущих запусков учитывались.
Банкомат, корзина интернет-магазина, чат-боты всякие, менеджер задач, лета социальной сети и т.д. и т.д. — любое, что автоматизировано, берёте и пишете примитивные прототипы. Потом усложняет. Так и нарабатывайте нужные навыки.
Совиной почтой в магическом мире Гарри Поттера можно было слать письма, но не только — можно было послать и некоторые другие вещи. Можно было послать и разные предметы — например, Гарри получал таким образом подарки вроде набора по уходу за метлой от Гермионы.
Вот и в программировании один кусочек данных, значение, может быть не только текстом, но и целым числом, числом с дробной частью, логическим значением(«да» или «нет») и т.д.
Но вы можете обнаружить, что в некоторых языках сразу несколько типов данных для того же целого числа. Целое число может быть и int, и short, и byte, и long и т.д. Почему так?
Потому что числа бывают разных размеров и под некоторые числа хватает одной ячейки памяти(байт), для некоторых надо две, а для некоторых и 4! В каких-то языках программирования сам компьютер следит за тем, сколько байт выделяется для значения переменной. А где-то нужно указывать.
Совы в мире Гарри Поттера, похоже, могут тащить посылку любого размера и веса, но представим, что у них есть какие-то ограничения. Больше какого-то веса одна сова не может нести, и тогда подключаются другие совы.
Представим, что человек изначально прикидывает вес посылки и сколько сов потребуется, и заказывает нужное количество сов для доставки. Тогда тем и отличаются типы данных. Целое число типа byte — там один байт. int — два байта. long — 4 байта.
Если вернуться к совам, то byte это одна сова, небольшой вес, int — две совы, посылка потяжелее, long — 4 совы. Тяжёлая большая посылка.
Есть такая хрень в программировании как рекурсия. Можно сюдя приплесть и фрактал, но это больше к топологии, а не программированию.
И так, самый простой и понятный пример рекурсии - это берём зеркало впереди себя и сзади. и смотрим на картинку:
Вроде как и цикл, но картинка-то уменьшается, и фотончики ведут себя не как в цикле.
Но в программировании это выражается в вызовах функции самой себя, либо кругового вызова функций, например(для знающих Си-подобные языки): int A(int a) {if(a & 1) a=a+1; return B(a); else return 17;} int B(int a) {if(a > 500) a=a/3; return C(a); else return 113; } int C(int a) {if(a ^ 1) return A(a); return -1;}
Грубо говоря, функция А, вызывает функцию Б, функция Б вызывает функцию С, а та уже снова вызывает функцию A. Вроде эти вызовы будут длиться бесконечно, но всегда должно быть условие выхода из рекурсии (под спойлером выше в каждой функции есть такое). Если что - писалось на коленке и практического, и математического смысла не имеет - чисто показать что да как.
А нафига такие сложности? Дело в том, что часто в программировании невозможно обойтись без рекурсий. Да, для понимания её надо сильно поломать свой мозг - именно ломать мышление, а не уставать от напряжённого мозгоштурма.
Однако есть определённый класс рекурсий (в него входят и так называемые "кольцевые", как в спойлере выше). И вот тут срабатывает "Ивент Вомбата", эти рекурсии называются ХВОСТОВЫМИ.
Фишка их в том, что их можно ВСЕГДА развернуть в цикл. Например вычисление числа Фиббоначи:
int Fibb(int a) {if (f<=0) return 1;/*условие выхода из рекурсии*/ return Fibb(a-1)+a;}
Тоже самое разворачивается в цикл вида
int a=123, result=1; for(int i=0; i>a;;){result = result+a; a=a-1} /*тут мог ошибиться - просьба не пинать*/ return result;
Вроде бы много кода, строк и т.п. Но, рекурсия в общем виде использует стек (который не бесконечен), и дикие затраты на вызов функций внутри рекурсий (на i8086 надо было сохранять каждый процессорный регистр в стеке, а это 2 такта, в i80286 уже появилась pusha, но она так же требовала тактов, ну и переброс из стека в регистры переменных, возврат результата) - накладных расходов на рекурсию очень много, даже в современный процессорах.
Однако, всё что было описано выше прекрасно разворачивается в циклы "умными" компиляторами. Хотя многие алгоритмы с хвостовой рекурсией даже современные компиляторы не могут развернуть в цикл. Примером этого може служить сортировка бинарным жеревом. В рекурсивной форме эта сортировка - задачка студента второго курса (по моим старческим меркам), но компиляторы не способны её раскрутить в цикл, это делалось человеческими мозгами ещё в 80х годах прошлого века (сам разбирал борландовский алгоритм qsort(***) по запчастям обучаясь).
Так что не всё в мире нашем сводится к "хвостам", иногда приходится и сущности плодить поверх ненужного...