Введение
Эта небольшая электронная книга предназначена для того, чтобы научить вас языку программирования под названием Forth (прим: вернее познакомить с пониманием некоторых основ Форт языка). Форт — это язык, непохожий на большинство других. Он не функциональный и не объектно-ориентированный, в нем нет проверки типов и практически отсутствует синтаксис. Он был написан в 70-х годах, но до сих пор используется для некоторых приложений .
Зачем учить такой странный язык? Каждый новый язык программирования, который Вы изучаете, помогает Вам думать о проблемах по-новому. Форт очень прост в освоении, но он требует от Вас мыслить не так, как вы привыкли. Это делает его идеальным языком для расширения ваших горизонтов кодирования.
Эта книга включает в себя простую реализацию Forth, которую я написал на JavaScript. Он ни в коем случае не совершенен, и ему не хватает многих функций, которые вы ожидаете от настоящей системы Forth. Это просто для того, чтобы дать вам простой способ опробовать примеры. (Если Вы эксперт по Forth, пожалуйста, поделитесь здесь и сделайте его лучше!)
(А, здесь код JS странички c Форт по представленному переводу для игры в Форт- Змейку[http://skilldrick.github.io/easyforth/]
Я предполагаю, что Вы знаете по крайней мере еще один язык программирования и имеете общее представление о том, как стеки работают как структуры данных.
Добавление некоторых чисел
Что отличает Forth от большинства других языков, так это использование стека. В Форте все вращается вокруг стека. Каждый раз, когда вы вводите число, оно помещается в стек. Если вы хотите сложить два числа вместе, +
введите два верхних числа из стека, сложите их и поместите результат обратно в стек.
Давайте посмотрим на пример. Введите (не копируйте и не вставляйте) следующее в интерпретатор, вводя Enter
после каждой строки.
1
2
3
Каждый раз, когда Вы вводите строку, за которой следует Enter
клавиша, интерпретатор Forth выполняет эту строку и добавляет строку, ok
чтобы Вы знали, что ошибок не было. Вы также должны заметить, что по мере выполнения каждой строки область вверху заполняется числами. Эта область является нашей визуализацией стека. Это должно выглядеть так:
1 2 3 <-
Теперь в том же интерпретаторе введите один +
а затем 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
выводящие i
if, 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/loop
s для рисования горизонтальных и вертикальных стен соответственно.
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 и.др.