
When I
wrote about the development of the game "
Thud! ", I already complained about some redundancy of the description received. The simplicity of the ZRF language has its downside - in order to write something more or less complicated on it, it is often necessary to duplicate significant code fragments. Such redundancy, as is known, leads not only to an increase in the amount of manual work, but also significantly increases the risk of various errors appearing in the code (since the process of debugging ZoG applications is not trivial, this is an essential point).
How can you deal with such redundancy?
Of course, with the help of macros! The problem is that the ZRF macros are not expressive enough for this. Adrian King, in the process of developing
Scirocco and
Typhoon games, came to a similar conclusion and developed his own, advanced macro language, working as an external
preprocessor . Today, I will talk about the possibilities of this language and try, using the example of Thud !, to show its use in the development of ZRF applications.
')
As I said above, this is an external preprocessor that converts the source files (with the extension .prezrf) into ordinary zrf files. The preprocessor itself is developed in the Java language and is a jar file. To process a prezrf file, just run the following command (provided that Java is installed on your computer):
java -jar prezrf.jar MyFile.prezrf
If processing passes without errors, the resulting zrf file will be generated in the same directory.
What opportunities does the new language offer us? First, it introduces a new type of macro. To define the macro prezrf, the
define! Keyword is used
. (to all new keywords, at the end, added an exclamation mark). The original
define ZRF macros are ignored by the preprocessor and are simply copied to the output. The definition of new sample macros can be canceled with the
undefine command
! . This feature can be useful, since new macros can be defined locally, in other macros (ZRF doesn’t allow doing this).
Keyword
expand! leads to the "deployment" of a previously defined macro in the place of the code indicated by it. Since this action is performed very often, the abbreviation '
! '. Thus, processing the following code:
(define! swap ($2 $1) ) (! swap ab)
... we get the output:
(ba)
This macro would have been expanded without an exclamation mark, using the (swap ab) command, but using
expand! saves us from possible typos. For example, if we, for some reason, forgot to add an exclamation mark to
define in the definition of swap, the construction (swap ab) would simply be duplicated in the output, and the macro call with the expansion
expand! , would lead to the formation of an error:
expand !: undefined macro "swap" .
All this, perhaps, would not be very interesting if it were not for the new features provided by prezrf. In macros of the new type, we can use list arguments! In addition, for our convenience, added the ability to reference several arguments passed to the macro, as in the list. The construction of
$ 2 * 4 sequentially displays the values ​​of the 2nd, 3rd, and 4th arguments passed to the macro (provided that at least four arguments are passed). Also, abbreviated constructions
$ n * and
$ * m with obvious semantics are defined. Using this feature, we can, for example, count the number of arguments passed to the macro:
(define! count (length! ($1*)) ) (! count abc) ; => 3
I draw your attention to the fact that the brackets around
$ 1 * , in this example are mandatory - we form a list in which we list the values ​​of all the arguments of the macro, starting with the first one. The absence of brackets will lead to a processing error, since
length! accepts only one list argument. However, our macro is not sufficiently protected from input errors. Calling
(count) with no arguments will result in an error. We can fix this as follows:
(define! count ($?1 (length! ($1*)) ) ($!1 0) )
Here
$? 1 is executed if one or more arguments are passed to the macro, and
$! 1 otherwise. In addition, it is possible to number elements from the end of the list using the
$ -n construct. All these features will be very useful to us in the future.
Like any self-respecting programming language, prezrf provides us with conditional execution constructs (
if-less!, If-less-or-equal! ) And a loop (
for! ). If-constructs (and there are several more of them in the language listed above), unlike the similar ZRF construct, do not define the
else branch, but
for! can only be used to crawl list items. For example, we can repeat the execution of some action for all directions defined in the game (this is required very often):
(define! -all-directions (n ne e se s sw w nw)) (define! shift-all (for! $d ($ -all-directions) (shift $d) ) ) (! shift-all)
In this code, the control construct '
$ ' is used, which I have not yet had time to talk about. What she does? In fact, this is an abbreviation for a very often used construction:
(!! (! macro))
expand! here we already know, but that means'
!! '? This command (
expand-first! ) Tells the preprocessor to use the value of the element, not the element itself in the parent construct (
for! In our example). This point may not be very clear, but it is very important for understanding the language. Here's what the output will look like if you use just
(! -All-directions) :
(shift !) (shift -all-directions)
This is clearly not what we wanted. Unfortunately
for! It works only with lists and cannot help us in optimizing the following outrage:
So Trolls go ( define troll-1 ( $1 (verify empty?) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-2 ( mark (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-3 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-4 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-5 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-6 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-7 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) )
But where cycles pass, recursion will come to our rescue:
Recursion, as was said (define! repeat (if-less! 0 $1 $2* (! repeat (!! (sum! $1 -1)) $2*) ) ) (define! troll-n (if-less! 0 $1 (for! $d ($ -all-directions) ( (if-less! 1 $1 mark (repeat $1 (opposite $d) (verify friend?) ) back ) (repeat $1 $d (verify empty?) ) (if-less! 1 $1 (verify (or (for! $dd ($ -all-directions) (enemy? $dd) ) ) ) ) (for! $dd ($ -all-directions) (if (enemy? $dd) (capture $dd) ) ) add ) ) (! troll-n (!! (sum! $1 -1))) ) ) ... (! troll-n 7)
This is somewhat more difficult to understand than the original version, but no copy-paste. Please pay special attention to the implementation of
repeat . This macro is very useful and will be used repeatedly in the future.
(! repeat 3 abc)
In the original implementation of Thud! There is another place that I would like to optimize:
( define check-rock ( check-rock-direction n ne e se s sw w nw) ( check-rock-direction ne e se s sw w nw n) ( check-rock-direction e se s sw w nw n ne) ( check-rock-direction se s sw w nw n ne e) ( check-rock-direction s sw w nw n ne e se) ( check-rock-direction sw w nw n ne e se s) ( check-rock-direction w nw n ne e se s sw) ( check-rock-direction nw n ne e se s sw w) )
Listing manually all cyclic permutations of a set of eight elements is easy to make mistakes. First of all, let's define a macro, which allows us to “cut out” a piece of the list:
(define! range (if-less-or-equal! $1 $2 (nth! $1 $3) (! range (!! (sum! $1 1)) $2*) ) ) (! range 3 4 (abcde)) ; => cd
The function
(nth! N list) , here, allows you to get the n-th element of the list. Let's try to "turn" the list:
(define! rotate (if-less! 0 $1 $2 (! rotate (!! (sum! $1 -1)) ((splice! ((! range 2 8 $2)) ((nth! 1 $2))))) ) ) (! rotate 2 (abcdefgh))
It seems to be normal, but already
(! Rotate 3 (abcdefgh)) gives an error:
Error messageExpanding [list "(nth! 2 ((splice! ((! Range 2 8 (abc" ... at the argument substitution in [list "(nth! $ 1 $ 3)" at t.prezrf, line 3] for [list "(range 2 8 ((splice! ((! Range 2 8 (ab "... at expand! Of [list" (! Range 2 8 ((splice! ((! Range 2 8 (a "... at the substitution in [list" ( ! range 2 8 $ 2) "at t.prezrf, line 11] for [list" (rotate 2 ((splice! ((! range 2 8 (ab "... at expand! of [list" (! rotate 2 ((splice ! ((! range 2 8 (a "... at argument substitution in [list" (! rotate (!!! (sum! $ 1 -1))) ((splice! ("... at t.prezrf, line 11] for [ list "(rotate 3 (abcdefgh))" at expand! of [list "(! rotate 3 (abcdefgh))" at t.prezrf, line 15]]]]]]]:
In [list "(nth! 2 ((splice! ((! Range 2 8 (abc" ... at the argument substitution in [list "(nth! $ 1 $ 3)" at t.prezrf, line 3] for [list "(range 2 8 ((splice! ((! Range 2 8 (ab "... at expand! Of [list" (! Range 2 8 ((splice! ((! Range 2 8 (a "... at the substitution in [list" ( ! range 2 8 $ 2) "at t.prezrf, line 11] for [list" (rotate 2 ((splice! ((! range 2 8 (ab "... at expand! of [list" (! rotate 2 ((splice ! ((! range 2 8 (a "... at argument substitution in [list" (! rotate (!!! (sum! $ 1 -1))) ((splice! ("... at t.prezrf, line 11] for [ list "(rotate 3 (abcdefgh))" at expand! of [list "(! rotate 3 (abcdefgh))" at t.prezrf, line 15]]]]]]]:
Trying to get item # 2 of list with 1 items
As you can see, it is not as extensive as error messages when compiling C ++ templates, but not much clearer. I have been fiddling with prezrf for quite a long time and brought out two rules of thumb for myself in terms of relatively painless work with him:
- Do not pass mutable lists as arguments to recursive macros.
- Limit the use of for! most simple cases
Any deviation from these rules, at times, threatens to blow my brain, in vain attempts to understand the essence of what happened. Let's try to rephrase our
rotate in the spirit of the first rule:
(define! rotate (if-less! 0 $1 ((splice! ((! range (!! (sum! $1 1)) (!! (length! $2)) $2)) ((! range 1 $1 $2)) )) (! rotate (!! (sum! $1 -1)) $2) ) (if-equal! 0 $1 $2 ) )
He began to look worse, but it works! What should we do with him now? Permutations we need for a reason, we must pass these arguments in the
check-rock-direction . Of course, it would be possible to make the appropriate changes to the
rotate , causing a macro from it, but this would make the
rotate not universal. Apparently, the time has come to uncover the secret weapon of functional programming —
functions of a higher order :
(define! map ($!3 (! map $1 $2 (!! (length! $2))) ) ($?3 (if-less! 0 $3 ($1 (!! (nth! $3 $2))) (! map $1 $2 (!! (sum! $3 -1))) ) ) ) (define check-rock (! map check-rock-direction (!! ((! rotate (!! (sum! (!! (length! ($ -all-directions))))) ($ -all-directions) ))) ) )
I had to tinker with this, but it was worth it. The main place in all this code is here:
($ 1 ...) . It also works, which can not but rejoice. Actually, it would be possible to stop on
this variant , if not the size of the resulting ZRF-file. Deploying all the macros from the 14-kilobyte source, the preprocessor receives at the output of more than a megabyte description file! Whether it is worth saying that ZoG loads this ZRF-ku quite slowly.
How to beat this trouble? Yes, with the help of macros (we have nothing else):
Macros create macros (define! troll-n (if-less! 0 $1 (define (concat! troll - $1) ( (if-less! 1 $1 mark (repeat (!! (sum! $1 -1)) (opposite (concat! $ 1)) (verify friend?) ) back ) (repeat $1 (concat! $ 1) (verify empty?) ) (if-less! 1 $1 (verify (or (for! $dd ($ -all-directions) (enemy? $dd) ) ) ) ) (for! $dd ($ -all-directions) (if (enemy? $dd) (capture $dd) ) ) add ) ) (! troll-n (!! (sum! $1 -1))) ) ) (! troll-n 7) (define! troll-all (if-less! 0 $1 (for! $d ($ -all-directions) ( (concat! troll - $1) $d ) ) (! troll-all (!! (sum! $1 -1))) ) )
The construction
(! Troll-n 7) sequentially creates definitions of the ZRF macros troll-1, troll-2, ... troll-7 (their order is not important for us), but
(! Troll-all) , being called in the right place, will list their calls. Here you should pay attention to the design
(concat! $ 1) . In such an intricate way, we form
$ 1 in the body of the ZRF macro. If we just say
$ 1 , it’s not very good.
The result is not slow to affect. The size of the resulting ZRF file is reduced to 28 kilobytes. It could have been made even less, but I didn’t see much sense in it. It works in the same way as the original, which means that we did not make mistakes on our long journey.
I want to note that the
preprocessor described by me
is completely free to use and cross-platform. Every happy owner of installed Java can experiment with it. Of course, the results of his labors will not be able to run in the demo version of ZoG, but we are not doing abnormal programming here?