Введение

Эта небольшая электронная книга предназначена для того, чтобы научить вас языку программирования под названием Forth (прим: вернее познакомить с пониманием некоторых основ Форт языка). Форт — это язык, непохожий на большинство других. Он не функциональный и не объектно-ориентированный, в нем нет проверки типов и практически отсутствует синтаксис. Он был написан в 70-х годах, но до сих пор используется для некоторых приложений .

Зачем учить такой странный язык? Каждый новый язык программирования, который Вы изучаете, помогает Вам думать о проблемах по-новому. Форт очень прост в освоении, но он требует от Вас мыслить не так, как вы привыкли. Это делает его идеальным языком для расширения ваших горизонтов кодирования.

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

(А, здесь код JS странички c Форт по представленному переводу для игры в Форт- Змейку[http://skilldrick.github.io/easyforth/]

Я предполагаю, что Вы знаете по крайней мере еще один язык программирования и имеете общее представление о том, как стеки работают как структуры данных.

Добавление некоторых чисел

Что отличает Forth от большинства других языков, так это использование стека. В Форте все вращается вокруг стека. Каждый раз, когда вы вводите число, оно помещается в стек. Если вы хотите сложить два числа вместе, +введите два верхних числа из стека, сложите их и поместите результат обратно в стек.

Давайте посмотрим на пример. Введите (не копируйте и не вставляйте) следующее в интерпретатор, вводя Enterпосле каждой строки.

1
2
3

Каждый раз, когда Вы вводите строку, за которой следует Enterклавиша, интерпретатор Forth выполняет эту строку и добавляет строку, okчтобы Вы знали, что ошибок не было. Вы также должны заметить, что по мере выполнения каждой строки область вверху заполняется числами. Эта область является нашей визуализацией стека. Это должно выглядеть так:

1 2 3 <-

Теперь в том же интерпретаторе введите single, +а затем Enterклавиша. Два верхних элемента в стеке 2и 3были заменены на 5.

1 5 <- ок

На этом этапе ваше окно редактора должно выглядеть так:

1 ок 2 ок 3 ок + ок

Введите +еще раз и нажмите Enter, и два верхних элемента будут заменены на 6. Если вы наберете +еще раз, Forth попытается извлечь два верхних элемента из стека, даже если в стеке только один элемент! Это приводит к Stack underflowошибке:

1 ок 2 ок 3 ок + ок + ок + опустошение стека

Форт не заставляет вас вводить каждый токен в виде отдельной строки. Введите следующее в следующий редактор, а затем клавишу Enter:

123 456 +

Теперь стек должен выглядеть так:

579 <- ок

Этот стиль, в котором оператор стоит после операндов, известен как обратная польская нотация . Давайте попробуем что-нибудь посложнее и посчитаем 10 * (5 + 2). Введите в интерпретатор следующее:

5 2 + 10 *

Одна из приятных особенностей Forth заключается в том, что порядок операций полностью зависит от их порядка в программе. Например, при выполнении 5 2 + 10 *интерпретатор помещает в стек 5, затем 2, затем складывает их и помещает в стек получившееся 7, затем помещает в стек 10, затем умножает 7 и 10. Из-за этого нет необходимости в круглых скобках для группировки. операторы с более низким приоритетом.

Эффекты стека

Большинство слов Forth так или иначе влияют на стек. Некоторые извлекают значения из стека, некоторые оставляют новые значения в стеке, а некоторые делают и то, и другое. Эти «эффекты стека» обычно представляются с помощью комментариев в форме ( до -- после ). Например, + ( n1 n2 -- sum )- n1и n2являются двумя верхними числами в стеке, и sumявляется значением, оставшимся в стеке.

Определение слов

Синтаксис Forth предельно прост. Форт-код интерпретируется как последовательность слов, разделенных пробелами. Почти все непробельные символы допустимы в словах. Когда интерпретатор Forth читает слово, он проверяет, существует ли определение во внутренней структуре, известной как словарь. Если он найден, это определение выполняется. В противном случае слово считается числом и помещается в стек. Если слово не может быть преобразовано в число, возникает ошибка.

Вы можете попробовать это самостоятельно ниже. Введите foo(неизвестное слово) и нажмите клавишу ввода.

Вы должны увидеть что-то вроде этого:

foo foo?

foo ?означает, что Forth не смог найти определение для foo, и это недопустимое число.

Мы можем создать собственное определение, fooиспользуя два специальных слова, называемых :(двоеточие) и ;(точка с запятой). :это наш способ сказать Форту, что мы хотим создать определение. Первое слово после :становится именем определения, а остальные слова (до ;) составляют тело определения. Обычно между именем и телом определения включают два пробела. Попробуйте ввести следующее:

: foo  100 + ;
1000 foo
foo foo foo

Предупреждение. Распространенной ошибкой является пропуск пробела перед ;словом. Поскольку слова Форта разделены пробелами и могут содержать большинство символов, +;это совершенно допустимое слово и не анализируется как два отдельных слова.

Как вы, надеюсь, уже поняли, наше fooслово просто добавляет 100 к значению на вершине стека. Это не очень интересно, но должно дать вам представление о том, как работают простые определения.

Управление стеком

Теперь мы можем приступить к рассмотрению некоторых предопределенных слов Forth. Во-первых, давайте посмотрим на некоторые слова для управления элементами в верхней части стека.

dup ( n -- n n )

dupявляется сокращением от «дубликат» — он дублирует верхний элемент стека. Например, попробуйте это:

1 2 3 dup

У вас должен получиться следующий стек:

1 2 3 3 <- ок ( верхний элемент стека самый праый в этом его представлении)

drop ( n -- )

dropпросто удаляет верхний элемент стека:

1 2 3 drop

сделает стек таким:

1 2 <- ок

swap ( n1 n2 -- n2 n1 )

swap, как вы уже догадались, меняет местами два верхних элемента стека. Например:

1 2 3 4 swap

даст тебе:

1 2 4 3 <- ок

over ( n1 n2 -- n1 n2 n1 )

overнемного менее очевиден: он берет второй элемент с вершины стека и дублирует его на вершину стека. Выполнение этого:

1 2 3 over

приведет к этому:

1 2 3 2 <- ок

rot ( n1 n2 n3 -- n2 n3 n1 )

Наконец, «вращает» триrot верхних элемента стека. Третий элемент с вершины стека перемещается на вершину стека, вытесняя два других элемента вниз.

1 2 3 rot

дает тебе:

2 3 1 <- ок

Генерация вывода

Далее рассмотрим некоторые слова для вывода текста в консоль.

. ( n -- ) (точка)

Самое простое выходное слово в Forth — это .. Вы можете использовать .для вывода вершины стека в выводе текущей строки. Например, попробуйте запустить это (не забудьте добавлять пробелы между символам!):

1 . 2 . 3 . 4 5 6 . . .

Вы должны увидеть это:

1 . 2 . 3 . 4 5 6 . . . 1 2 3 6 5 4 ок

Проходя это по порядку, мы нажимаем 1, затем выталкиваем его и выводим. Затем делаем то же самое с 2и 3. Затем мы помещаем 4, 5и 6в стек. Затем мы выталкиваем их и выводим по одному. Вот почему последние три числа в выводе меняются местами: стек вошел последним, вышел первым.

emit ( c -- )

emitможет использоваться для вывода чисел в виде символов ASCII. Точно так же, как .выводит число наверху стека, emitвыводит это число как символ ascii. Например:

 33 119 111 87 emit emit emit emit

Я не буду приводить вывод здесь, чтобы не испортить сюрприз. Это также может быть записано как:

87 emit 111 emit 119 emit 33 emit

В отличие от ., emitне выводит пробел после каждого символа, что позволяет создавать произвольные строки вывода.

cr ( -- )

crявляется сокращением от возврата каретки — он просто ререводит вывод на новую строку:

cr 100 . cr 200 . cr 300 .

Эта последовательность:

100 cr . 200 cr . 300 cr .
100
200
300 ок

." ( -- )

Наконец у нас есть ."— специальное слово для вывода строк. Слово ."работает по-разному внутри определений в интерактивном режиме. ."отмечает начало строки для вывода, а конец строки определяется по ".
"— это контекстное слово для использоания, поэтому его не нужно разделять пробелами. Вот пример:

: say-hello  ." Hello World!" ;
say-hello

Вы должны увидеть следующий вывод

Hello World!

Мы можем комбинировать .", ., crи emitдля создания более сложного вывода:

: print-stack-top  cr dup ." Вершина стека - " .
  cr ." ,что выглядит как '" dup emit ." ' в ascii  " ;
48 print-stack-top

Запуск этого должен дать вам следующий результат:

48 print-stack-top
Вершина стека - 48, что выглядит как 0 в ascii ok

Условные операторы и циклы

Теперь о самом интересном! Форт, как и большинство других языков, имеет условные операторы и циклы для управления потоком вашей программы. Однако, чтобы понять, как они работают, сначала нам нужно понять булевы значения в Forth.

Булевы значения

На самом деле в Forth нет логического типа. Число 0считается ложным, а любое другое число — истинным, хотя каноническим истинным значением является -1(все логические операторы возвращают 0или -1).

Чтобы проверить, равны ли два числа, вы можете использовать =:

3 4 = .
5 5 = .

Это должно вывести:

3 4 = . 0 ок 
5 5 = .  -1 ок

Вы можете использовать <and >для меньшего и большего чем.
<проверяет, меньше ли второй элемент с вершины стека, чем верхний элемент стека, и наоборот >:

3 4 < .
3 4 > .

3 4 < . -1 ок 3 4 > . 0 ок

Логические операторы And, Or и Not доступны как and, or, и invert:

3 4 < 20 30 < and .
3 4 < 20 30 > or .
3 4 < invert .

Первая строка эквивалентна 3 < 4 & 20 < 30на языке C. Вторая строка эквивалентна 3 < 4 | 20 > 30. Третья строка эквивалентна !(3 < 4).

and, or, и invertвсе побитовые операции. Для правильно сформированных флагов ( 0и -1) они будут работать, как и ожидалось, но будут давать неверные результаты для произвольных чисел.

if then

Теперь мы наконец можем перейти к условным выражениям. Условные выражения в Forth могут использоваться только внутри определений. Простейшим условным оператором в Forth является if then, который эквивалентен стандартному ifоператору в большинстве языков. Вот пример определения с использованием if then. В этом примере мы также используем слово mod, которое возвращает модуль двух верхних чисел в стеке. В этом случае верхнее число — 5, а второе — то, что было помещено в стек перед вызовом buzz?. Следовательно, 5 mod 0 =это логическое выражение, которое проверяет, делится ли число на вершине стека на 5.

: buzz?  5 mod 0 = if ." Buzz" then ;
3 buzz?
4 buzz?
5 buzz?

Это выведет:

3 buzz? ок
4 buzz? ок
5 buzz? Buzz ok

Важно отметить, что thenслово отмечает конец утверждения if. Это делает его эквивалентным, например, fiв Bash или Ruby.end

Еще одна важная вещь, которую следует осознать, заключается в том, что он ifне оставляет верхнее значение в стеке, когда проверяет, является ли оно истинным или ложным.

if else then

if else thenэквивалентно оператору if/elseв большинстве языков. Вот пример его использования:

: is-it-zero?  0 = if ." Да!" else ." Нет!" then ;
0 is-it-zero?
1 is-it-zero?
2 is-it-zero?

Это выводит:

0 Да! ок
1 Нет! ок
2 Нет! ок

На этот раз предложение if (консеквент) — это все, что находится между ifи else, а предложение else (альтернативное) — это все, что находится между elseи then.

do loop

do loopв Forth больше всего напоминает forцикл в большинстве языков на основе C. В теле a do loopспециальное слово iпомещает текущий индекс цикла в стек.

Два верхних значения в стеке дают начальное значение (включительно) и конечное значение (исключая) для значения i. Начальное значение берется с вершины стека. Вот пример:

: loop-test  10 0 do i . loop ;
loop-test

Это должно вывести:

0 1 2 3 4 5 6 7 8 9 ок

Выражение 10 0 do i . loopпримерно эквивалентно:

for (int i = 0; i < 10; i++) {
  print(i);
}

Физз Базз

Мы можем легко написать классическую программу Fizz Buzz , используя do loop:

: fizz?  3 mod 0 = dup if ." Fizz" then ;
: buzz?  5 mod 0 = dup if ." Buzz" then ;
: fizz-buzz?  dup fizz? swap buzz? or invert ;
: do-fizz-buzz  25 1 do cr i fizz-buzz? if i . then loop ;
do-fizz-buzz

fizz?проверяет, делится ли вершина стека на 3, используя 3 mod 0 =. Затем он использует dupдля дублирования флага результата. Верхняя копия значения потребляется if. Вторая копия остается в стеке и действует как возвращаемое значение fizz?.

Если число на вершине стека делится на 3, "Fizz"будет выведена строка, иначе вывода не будет.

buzz?делает то же самое, но с 5, и выводит строку "Buzz".

fizz-buzz?вызывает dupдублирование значения на вершине стека, затем вызывает fizz?, преобразуя верхнюю копию в логическое значение. После этого вершина стека состоит из исходного значения и логического значения, возвращаемого fizz?. swapменяет их местами, поэтому исходное значение вершины стека снова оказывается наверху, а логическое значение — под ним. Затем мы вызываем buzz?, который заменяет значение вершины стека логическим флагом. Теперь два верхних значения в стеке являются логическими значениями, представляющими, делится ли число на 3 или 5. После этого мы вызываем, orчтобы проверить, верно ли какое-либо из них, и invertотменить это значение. Логически тело fizz-buzz?эквивалентно:

!(x % 3 == 0 || x % 5 == 0)

Следовательно, fizz-buzz?возвращает логическое значение, указывающее, не делится ли аргумент на 3 или 5 и, следовательно, должен быть напечатан. Наконец, do-fizz-buzzциклы от 1 до 25, вызывающие fizz-buzz?и iвыводящие iif, fizz-buzz?возвращают true.

Если у вас возникли проблемы с пониманием того, что происходит внутри fizz-buzz?, приведенный ниже пример может помочь вам понять, как это работает. Все, что мы здесь делаем, — это выполняем каждое слово определения fizz-buzz?в отдельной строке. При выполнении каждой строки наблюдайте за стеком, чтобы увидеть, как он меняется:

: fizz?  3 mod 0 = dup if ." Fizz" then ;
: buzz?  5 mod 0 = dup if ." Buzz" then ;
4
dup
fizz?
swap
buzz?
or
invert

Вот как каждая строка влияет на стек:

4         4 <- ок
dup       4 4 <- ок
fizz?     4 0 <- ок
swap      0 4 <- ок
buzz?     0 0 <- ок
or        0 <- ок
invert    -1 <- ок

Помните, что последнее значение в стеке — это возвращаемое значение слова fizz-buzz?. В данном случае верно, потому что число не делилось ни на 3, ни на 5, а значит должно быть напечатано.

Вот то же самое, но начиная с 5:

5         5 <- ок
dup       5 5 <- ок
fizz?     5 0 <- ок
swap      0 5 <- ок
buzz?     0 -1 <- ок
or        -1 <- ок
invert    0 <- ок

В этом случае исходное значение вершины стека делилось на 5, поэтому ничего не нужно печатать.

Переменные и константы

Форт также позволяет сохранять значения в переменных и константах. Переменные позволяют отслеживать изменяющиеся значения, не сохраняя их в стеке. Константы дают вам простой способ сослаться на значение, которое не изменится.

Переменные

Поскольку роль локальных переменных обычно играет стек, переменные в Forth больше используются для хранения состояния, которое может понадобиться для нескольких слов.

Определить переменные просто:

variable balance

Это в основном связывает определенную ячейку памяти с именем balance. balanceтеперь является словом, и все, что он делает, — это помещает адрес ячейки памяти в стек:

variable balance
balance

Вы должны увидеть значение 1000в стеке. (в реализации EasyForth на JS) Эта реализация Forth произвольно начинает сохранять переменные в ячейке памяти 1000.

Слово !сохраняет значение в ячейке памяти, на которую ссылается переменная, и слово @извлекает значение из ячейки памяти:

variable balance
123 balance !
balance @

На этот раз вы должны увидеть значение 123в стеке. 123 balanceпомещает значение и ячейку памяти в стек и !сохраняет это значение в этой ячейке памяти. Аналогичным образом @извлекает значение на основе расположения в памяти и помещает это значение в стек. Если вы использовали C или C++, вы можете думать об balanceуказателе, который разыменовывается с помощью @.

Слово ?определяется как @ .и печатает текущее значение переменной. Это слово +!используется для увеличения значения переменной на определенную величину (как +=в языках на основе C).

variable balance
123 balance !
balance ?
50 balance +!
balance ?

Запустите этот код, и вы должны увидеть:

123 ок
173 ok

Константы

Если у вас есть значение, которое не меняется, вы можете сохранить его как константу. Константы определяются в одной строке, например:

42 constant answer

Это создает новую константу, вызываемую answerсо значением 42. В отличие от переменных, константы просто представляют значения, а не ячейки памяти, поэтому нет необходимости использовать @.

42 constant answer
2 answer *

Выполнение этого поместит значение 84в стек. answerобрабатывается так, как если бы это было число, которое оно представляет (точно так же, как константы и переменные в других языках).

Массивы

Forth не совсем поддерживает массивы, но он позволяет вам выделять место непрерывной памяти, очень похоже на массивы в C. Чтобы выделить эту память, используйте слово allot.

variable numbers
3 cells allot
10 numbers 0 cells + !
20 numbers 1 cells + !
30 numbers 2 cells + !
40 numbers 3 cells + !

В этом примере создается ячейка памяти с именем numbersи резервируются три дополнительные ячейки памяти после этой ячейки, что дает в общей сложности четыре ячейки памяти. ( cellsпросто умножается на размер ячейки, которая в этой реализации равна 1.)

numbers 0 +дает адрес первой ячейки в массиве. 10 numbers 0 + !сохраняет значение 10в первой ячейке массива.

Мы можем легко написать слова, чтобы упростить доступ к массиву:

variable numbers
3 cells allot
: number  ( offset -- addr )  cells numbers + ;

10 0 number !
20 1 number !
30 2 number !
40 3 number !

2 number ?

numberпринимает смещение numbersи возвращает адрес памяти по этому смещению. 30 2 number !сохраняет 30по смещению 2в numbers, и 2 number ?печатает значение по смещению 2в numbers.

Ввод с клавиатуры

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

key . key . key .

Когда вы запустите эту строку, вы заметите, что сначала ничего не происходит. Это связано с тем, что интерпретатор ожидает вашего ввода с клавиатуры. Попробуйте нажать Aклавишу, и вы должны увидеть код клавиши для этой клавиши, 65появившийся в качестве вывода в текущей строке. Теперь нажмите B, затем C, и вы должны увидеть следующее:

65 66 67 ок

Печать нажатых клавиш с begin until

Форт имеет другой вид цикла, который называется begin until. Это работает как whileцикл в языках на основе C. Каждый раз, когда попадается слово until, интерпретатор проверяет, является ли вершина стека отличной от нуля (истина). Если это так, он переходит к выполнению кода за until. Если нет, выполнение продолжается c соотетствующего begin.

Вот пример использования begin untilдля печати кодов клавиш:

: print-keycode  begin key dup . 32 = until ;
print-keycode

Это будет продолжать печатать коды клавиш, пока вы не нажмете пробел. Вы должны увидеть что-то вроде этого:

print-keycode 80 82 73 78 84 189 75 69 89 67 79 68 69 32 ок

keyждет ввода клавиши, затем dupдублирует код её полученой из key. Затем мы используем .для вывода верхней копии кода клавиши и 32 =проверяем, равен ли код клавиши 32. Если это так, мы выходим из цикла, в противном случае возвращаемся к begin.

Змейка!

Теперь пришло время собрать все воедино и сделать игру! Вместо того, чтобы вы вводили весь код, я предварительно загрузил его в редактор.

Прежде чем мы посмотрим на код, попробуйте поиграть в игру. Чтобы начать игру, выполните слово start. Затем используйте клавиши со стрелками, чтобы переместить змею. Если вы проиграете, вы можете ввести startснова.
После символа \ некоторые комментарии кода программы

<- start

variable snake-x-head  

500 cells allot  

variable snake-y-head  

500 cells allot  

variable apple-x  
variable apple-y  

0 constant left  
1 constant up  
2 constant right  
3 constant down  
24 constant width  
24 constant height  

variable direction  
variable length  

: snake-x ( offset -- address ) 
  cells snake-x-head + ;  

: snake-y ( offset -- address ) 
  cells snake-y-head + ;  ok

: convert-x-y ( x y -- offset )  24 cells * + ;  

: draw ( color x y -- )  convert-x-y graphics + ! ;  

: draw-white ( x y -- )  1 rot rot draw ;  

: draw-black ( x y -- )  0 rot rot draw ;  

: draw-walls 
  width 0 do 
    i 0 draw-black 
    i height 1 - draw-black 
  loop 
  height 0 do 
    0 i draw-black 
    width 1 - i draw-black 
  loop ;  

: initialize-snake 
  4 length ! 
  length @ 1 + 0 do 
    12 i - i snake-x ! 
    12 i snake-y ! 
  loop 
  right direction ! ;  

: set-apple-position apple-x ! apple-y ! ;  

: initialize-apple  4 4 set-apple-position ;  

: initialize 
  width 0 do 
    height 0 do 
      j i draw-white 
    loop 
  loop 
  draw-walls 
  initialize-snake 
  initialize-apple ;  
  
: move-up  -1 snake-y-head +! ;  

: move-left  -1 snake-x-head +! ;  

: move-down  1 snake-y-head +! ;  

: move-right  1 snake-x-head +! ;  

: move-snake-head  direction @ 
  left over  = if move-left else 
  up over    = if move-up else 
  right over = if move-right else 
  down over  = if move-down 
  then then then then drop ;  
  
\ Переместите каждый сегмент змеи на один шаг вперед  

: move-snake-tail  0 length @ do 
    i snake-x @ i 1 + snake-x ! 
    i snake-y @ i 1 + snake-y ! 
  -1 +loop ;  

: is-horizontal  direction @ dup 
  left = swap 
  right = or ; 

: is-vertical  direction @ dup 
  up = swap 
  down = or ;  

 : turn-up     is-horizontal if up direction ! then ;  

: turn-left   is-vertical if left direction ! then ;  

: turn-down   is-horizontal if down direction ! then ;  

: turn-right  is-vertical if right direction ! then ;  

 : change-direction ( key -- ) 
  37 over = if turn-left else 
  38 over = if turn-up else 
  39 over = if turn-right else 
  40 over = if turn-down 
  then then then then drop ;  

: check-input 
  last-key @ change-direction 
  0 last-key ! ;  

\ получить случайную позицию x или y в пределах игровой области  

: random-position ( -- pos ) 
  width 4 - random 2 + ;  

: move-apple 
  apple-x @ apple-y @ draw-white 
  random-position random-position 
  set-apple-position ;  

: grow-snake  1 length +! ;  

: check-apple 
  snake-x-head @ apple-x @ = 
  snake-y-head @ apple-y @ = 
  and if 
    move-apple 
    grow-snake 
  then ; 

: check-collision ( -- flag ) 
  \ получить текущую позицию x/y
  snake-x-head @ snake-y-head @ 
  \ получить цвет в текущей позиции 
  convert-x-y graphics + @ 
  \ оставить булевский флаг в стеке
  0 = ; 

: draw-snake 
  length @ 0 do 
    i snake-x @ i snake-y @ draw-black 
  loop 
  length @ snake-x @ 
  length @ snake-y @ 
  draw-white ;  ok

 : draw-apple 
  apple-x @ apple-y @ draw-black ;  

: game-loop ( -- ) 
  begin 
    draw-snake 
    draw-apple 
    100 sleep 
    check-input 
    move-snake-tail 
    move-snake-head 
    check-apple 
    check-collision 
  until 

  ." Game Over" ;  
  
: start  initialize game-loop ;  
  ok

Прежде чем мы слишком углубимся в этот код, два заявления об отказе от ответственности. Во-первых, это ужасный Форт-код. Я ни в коем случае не эксперт по Forth, так что, вероятно, многие вещи я делаю совершенно не так.
Во-вторых, эта игра использует несколько нестандартных методов взаимодействия с JavaScript. Я пройдусь по ним сейчас.

Нестандартные дополнения

Холст

Возможно, вы заметили, что этот "редактор" отличается от других: в него встроен элемент Canvas HTML5. Я создал очень простой интерфейс с отображением памяти для рисования на этом холсте. Холст разделен на 24 x 24 «пикселя», которые могут быть черными или белыми. Первый пиксель находится по адресу памяти, заданному переменной graphics, а остальные пиксели являются смещениями от переменной. Так, например, чтобы нарисовать белый пиксель в верхнем левом углу, вы можете запустить

1 graphics !

В игре используются следующие слова для рисования на холсте:

: convert-x-y ( x y -- offset )  24 cells * + ;
: draw ( color x y -- )  convert-x-y graphics + ! ;
: draw-white ( x y -- )  1 rot rot draw ;
: draw-black ( x y -- )  0 rot rot draw ;

Например, 3 4 draw-whiteрисует белый пиксель с координатами (3, 4). Координата y умножается на 24, чтобы получить строку, затем добавляется координата x, чтобы получить столбец.

Неблокирующий ввод с клавиатуры

Слово Форт keyблокирует ввод, поэтому не подходит для такой игры. Я добавил переменную last-key, которая всегда содержит значение последней нажатой клавиши. last-keyобновляется только тогда, когда интерпретатор выполняет код Forth.

Генерация случайных чисел

Стандарт Forth не определяет способ генерации случайных чисел, поэтому я добавил слово с именем, random ( range -- n )которое принимает диапазон и возвращает случайное число от 0 до диапазона - 1. Например, 3 randomможет возвращать 0, 1, или 2.

sleep ( ms -- )

Наконец, я добавил блокирующее sleepслово, которое приостанавливает выполнение на заданное количество миллисекунд.

Код игры

Теперь мы можем работать с кодом от начала до конца.

Переменные и константы

Начало кода просто устанавливает некоторые переменные и константы:

variable snake-x-head
500 cells allot

variable snake-y-head
500 cells allot

variable apple-x
variable apple-y

0 constant left
1 constant up
2 constant right
3 constant down

24 constant width
24 constant height

variable direction
variable length

snake-x-headи snake-y-headявляются ячейками памяти, используемыми для хранения координат x и y головы змеи. После этих двух мест выделено 500 ячеек памяти для хранения координат хвоста змеи.

Затем мы определяем два слова для доступа к ячейкам памяти, представляющим тело змеи.

: snake-x ( offset -- address )
  cells snake-x-head + ;

: snake-y ( offset -- address )
  cells snake-y-head + ;

Как и numberпредыдущее слово, эти два слова используются для доступа к элементам в массивах сегментов змеи. После этого идут слова для рисования на холсте, описанные выше.

Мы используем константы для обозначения четырех направлений ( left, up, rightи down) и переменную directionдля хранения текущего направления.

Инициализация

После этого инициализируем все:

: draw-walls
  width 0 do
    i 0 draw-black
    i height 1 - draw-black
  loop
  height 0 do
    0 i draw-black
    width 1 - i draw-black
  loop ;

: initialize-snake
  4 length !
  length @ 1 + 0 do
    12 i - i snake-x !
    12 i snake-y !
  loop
  right direction ! ;

: set-apple-position apple-x ! apple-y ! ;

: initialize-apple  4 4 set-apple-position ;

: initialize
  width 0 do
    height 0 do
      j i draw-white
    loop
  loop
  draw-walls
  initialize-snake
  initialize-apple ;

draw-wallsиспользует два do/loops для рисования горизонтальных и вертикальных стен соответственно.

initialize-snakeустанавливает lengthпеременную в 4, затем зацикливается от 0до length + 1заполнения начальных позиций змеи. Позиции змеи всегда остаются на единицу длиннее, чем длина, поэтому мы можем легко вырастить змею.

set-apple-positionи initialize-appleустановите начальное положение яблока на (4,4).

Наконец, initializeзаполняет все белым цветом и вызывает три слова инициализации.

Перемещение змеи

Вот код для перемещения змеи на основе текущего значения direction:

: move-up  -1 snake-y-head +! ;
: move-left  -1 snake-x-head +! ;
: move-down  1 snake-y-head +! ;
: move-right  1 snake-x-head +! ;

: move-snake-head  direction @
  left over  = if move-left else
  up over    = if move-up else
  right over = if move-right else
  down over  = if move-down
  then then then then drop ;

\ Переместите каждый сегмент змеи на один шаг вперед
: move-snake-tail  0 length @ do
    i snake-x @ i 1 + snake-x !
    i snake-y @ i 1 + snake-y !
  -1 +loop ;

move-up, move-left, move-down, и move-rightпросто добавьте или вычтите единицу из координат x или y головы змеи. move-snake-headпроверяет значение directionи вызывает соответствующее move-*слово. Этот over = ifшаблон представляет собой идиоматический способ выполнения операторов case в Forth.

move-snake-tailпроходит по массиву позиций змейки в обратном направлении, копируя каждое значение вперед на 1 ячейку. Это вызывается до того, как мы переместим голову змеи, чтобы переместить каждый сегмент змеи вперед на одну позицию. Он использует do/+loopвариант a do/loop, который извлекает стек на каждой итерации и добавляет это значение к следующему индексу вместо того, чтобы каждый раз увеличиваться на 1. Итак 0 length @ do -1 +loop, циклы от lengthдо 0с шагом -1.

Ввод с клавиатуры

Следующий раздел кода принимает ввод с клавиатуры и при необходимости изменяет направление змейки.

: is-horizontal  direction @ dup
  left = swap
  right = or ;

: is-vertical  direction @ dup
  up = swap
  down = or ;

: turn-up     is-horizontal if up direction ! then ;
: turn-left   is-vertical if left direction ! then ;
: turn-down   is-horizontal if down direction ! then ;
: turn-right  is-vertical if right direction ! then ;

: change-direction ( key -- )
  37 over = if turn-left else
  38 over = if turn-up else
  39 over = if turn-right else
  40 over = if turn-down
  then then then then drop ;

: check-input
  last-key @ change-direction
  0 last-key ! ;

is-horizontalи is-verticalпроверьте текущий статус переменной direction, чтобы увидеть, является ли она горизонтальным или вертикальным направлением.

Слова turn-*используются для установки нового направления, но используйте is-horizontalи is-verticalдля проверки текущего направления, чтобы убедиться, что новое направление допустимо. Например, если змея движется горизонтально, установка нового направления leftили rightне имеет смысла.

change-directionберет клавишу и называет соответствующее turn-*слово, если клавиша была одной из клавиш со стрелками. check-inputвыполняет работу по получению последней клавиши из last-keyпсевдопеременной, вызывает change-direction, затем устанавливает last-keyзначение 0, чтобы указать, что последнее нажатие клавиши было обработано.

Яблоко

Следующий код используется для проверки того, было ли яблоко съедено, и если да, то для перемещения его в новое (случайное) место. Кроме того, если яблоко было съедено, мы выращиваем змею.

\ получить случайную позицию x или y в пределах игровой области
: random-position ( -- pos )
  width 4 - random 2 + ;

: move-apple
  apple-x @ apple-y @ draw-white
  random-position random-position
  set-apple-position ;

: grow-snake  1 length +! ;

: check-apple ( -- flag )
  snake-x-head @ apple-x @ =
  snake-y-head @ apple-y @ =
  and if
    move-apple
    grow-snake
  then ;

random-positionгенерирует случайную координату x или y в диапазоне от 2до width - 2. Это предотвращает появление яблока рядом со стеной.

move-appleстирает текущее яблоко (используя draw-white), затем создает новую пару координат x/y для яблока, используя random-positionдважды. Наконец, он призывает set-apple-positionпереместить яблоко в новые координаты.

grow-snakeпросто добавляет единицу к lengthпеременной.

check-appleсравнивает координаты x/y яблока и головы змеи, чтобы увидеть, совпадают ли они (используя =дважды и andобъединяя два логических значения). Если координаты совпадают, вызываем move-appleпереместить яблоко на новую позицию и grow-snakeсделать змею на 1 сегмент длиннее.

Обнаружение столкновений

Далее мы видим, столкнулась ли змея со стенами или сама с собой.

: check-collision ( -- flag )
  \ get current x/y position
  snake-x-head @ snake-y-head @

  \ get color at current position
  convert-x-y graphics + @

  \ leave boolean flag on stack
  0 = ;

check-collisionпроверяет, является ли новая позиция головы змеи уже черной (это слово вызывается после обновления позиции змеи, но перед ее отрисовкой в новой позиции). Мы оставляем логическое значение в стеке, чтобы сказать, произошло столкновение или нет.

Рисуем змею и яблоко

Следующие два слова отвечают за рисование змеи и яблока.

: draw-snake
  length @ 0 do
    i snake-x @ i snake-y @ draw-black
  loop
  length @ snake-x @
  length @ snake-y @
  draw-white ;

: draw-apple
  apple-x @ apple-y @ draw-black ;

draw-snakeперебирает каждую ячейку в массивах змей, рисуя для каждой черный пиксель. После этого он рисует белый пиксель со смещением length. Последняя часть хвоста находится length - 1в массиве, поэтому lengthсодержит предыдущий последний сегмент хвоста.

draw-appleпросто рисует черный пиксель в текущем местоположении яблока.

Игровой цикл

Игровой цикл постоянно зацикливается до тех пор, пока не произойдет столкновение, по очереди вызывая каждое из слов, определенных выше.

: game-loop ( -- )
  begin
    draw-snake
    draw-apple
    100 sleep
    check-input
    move-snake-tail
    move-snake-head
    check-apple
    check-collision
  until
  ." Game Over" ;

: start  initialize game-loop ;

Цикл begin/untilиспользует логическое значение, возвращаемое параметром, check-collisionчтобы узнать, следует ли продолжать выполнение цикла или выйти из цикла. При выходе из цикла строка "Game Over"печатается. Мы используем 100 sleepпаузу на 100 мс на каждой итерации, заставляя игру работать со скоростью примерно 10 кадров в секунду.

startпросто вызывает initialize, чтобы сбросить все, а затем стартует game-loop. Поскольку вся инициализация происходит в initializeслове, вы можете снова вызвать startпосле окончания игры.


Вот и все! Надеюсь, весь код в игре имел смысл. Если нет, вы можете попробовать запустить отдельные слова, чтобы увидеть их влияние на стек и/или на переменные.

Конец

Форт на самом деле намного мощнее, чем то, чему я здесь учил (и то, что я реализовал в своем интерпретаторе). Настоящая система Forth позволяет вам изменять работу компилятора и создавать новые определяющие слова, что позволяет вам полностью настраивать вашу среду и создавать свои собственные языки в Forth.

Отличным ресурсом для изучения всей мощи Форта является короткая книга «Начало Форта» Лео Броди. Он доступен бесплатно в Интернете и на русском языке и научит вас всему интересному, что я пропустил. Также есть хороший набор упражнений, чтобы проверить свои знания. Однако вам нужно скачать копию SwiftForth , чтобы запустить код

Примечание: или другую подходящюю Форт систему на выбор из большого их ряда как, к примеру, Win32Forth, VFX Forth, SP-Forth, BigForth, gForth, kForth, pForth ...
или, к примеру, их реализации в составе какой то фантазийной игровой консоли как Retro-40 и.др.

Комментарии (8)


  1. begemot_sun
    01.06.2023 06:35
    +4

    Самая лучшая книга для новичков: Лео Броуди - НАЧАЛЬНЫЙ КУРС ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ ФОРТ, 1990. С весёлыми картинками.


  1. Jury_78
    01.06.2023 06:35
    +1

    Помню на Радио-86РК был Форт, даже пробовал какие то программы писать.


  1. Imp5
    01.06.2023 06:35
    +2

    а затем Enterключ

    Это не перевод, а просто кусок мусора.


  1. IsKaropki
    01.06.2023 06:35

    Не надо, барин.


  1. Gryphon88
    01.06.2023 06:35

    Не подскажете, почему операция в Форте операция применяется к паре операндов на стеке, а не как в Лиспе, ко всему в скобочках или на стеке, если скобочек нет? Ну и не очень понимаю, зачем форт сейчас, форт-машина лучше всего реализуется на стековых cpu, которые сейчас не выпускаются.


    PS понимаю, что это нормальный подход при обучении, но на строке "Мы реализовали Forth на JS" у меня глазик задергался. Язык, сопоставимый по скорости исполнения с асмом, на жс...


    1. forthuse Автор
      01.06.2023 06:35
      +1

      Форт так в базисе своих устоявшихся возможностей спроектирован, но ничего не мешает сделать из него произвольный DSL на его ядре (как и Lisp на Форт есть) оставаясь ещё в каком то базисе его понимания для использования. В дизайне Форт системы и стандарта языка много чего существует и для, к примеру, применения методик метапрограммирования.

      Есть возможность и к многим значениям на стеке сделать слова применения какой то операции, но при этом или задавать количество операндов к обработке на стеке или маркировать глубину параметров или ещё как то применяя к примеру слово DEPTH (количество элементов на стеке).

      Форт достаточно неплохо реализуется и работает и не на стековых CPU в использовании своей спецификации.

      P.S. То, что он в этом и другом варианте как проекты https://github.com/JohnEarnest/Mako , jeforth.3we реализованы на JS нет ничего страшного т.к. сферы применения Форт достаточно разнообразны. К примеру на базисе jeForth сделан и чатАИ бот со встроенным Форт языком https://github.com/hcchengithub/ChatFORTH


  1. eaa
    01.06.2023 06:35

    Напомнило программирование на калькуляторе мк-61


    1. forthuse Автор
      01.06.2023 06:35

      Отчасти это так, но у МК-61 нет полноценного стека данных как у Форт и других возможностей Форт.

      Но, тем не менее, и для продолжения ПМК - МК161 умельцами сделан Форт (eForth) на ПМК системе команд https://habr.com/ru/articles/452398/

      P.S. Для калькулятора HP-71B (близок по возможностям и комплектации к отнесению к миникомпьютерам) был в составе его и Forth. Дата выпуска этого калькулятора 1984г.

      Телеграм канал - МК61 МК52 MK85 Развиваем легендарные советские программируемые калькуляторы

      Телеграм канале по Форт

      [TF] Форт и общение фортеров

      Обсуждение конкатенативных языков программирования и тематическое общение программистов.

      Действующий форум по Forth (Форт) и другим саморасширяющимся системам программирования