
Some time ago, inspired by the reading of SICP, I wrote a couple of my implementations of Lisp-like interpreters with strict semantics, added a desktop GUI, a console interface, wrote Tetris on it and much more, and published a couple of articles on Habr about it.
Recently, I added the opportunity for a wide audience to get acquainted with this language - I wrote REPL bots for the following messengers: IRC, Telegram, Slack, Gitter. The bots are located on channels specially created for them, but in most cases they can be added / invited to other channels and you can conduct personal correspondence with them. This format allows for online textual reports on the basics of functional programming, accompanied by a real-time demonstration of the interpreter.
')
Of course, graphic windows with animation can be created only in the desktop version of the application. Therefore, for a greater disclosure of the possibilities of language and REPL, I wrote a textual implementation of the game Maze, in which any number of people can play with the bot. Details and some lyrics under the cut.
Description and rules of the game
When I was a schoolboy (and these were the 80s of the last century), computers and the Internet were, to put it mildly, not as widespread and accessible as they are now. Therefore, friends and classmates and I played normal children's games with a pen on a piece of paper — sea battle, dots, etc. Among others there was such a game, which we called the Labyrinth.
The rules are as follows: the leader is selected, who makes a map and draws it on his piece of paper, without showing it to anyone. The card is a rectangular grid n for m cells, the cell can be empty, there can be a pit in the cell - an object of the teleport type - when it gets into one cell of the pit the player moves to another and the leader calls the player the number of the pit, there are rivers on the map - they flow only in adjacent cells (diagonally not allowed), when it enters the river, the player is transferred to the end of the river and the leader informs the player of the fact of the swim
without specifying the river number . When you try to go in the direction of the wall, the presenter says this. Map parameters, number of rivers / holes and length of rivers are known to all players. At the beginning of the game, each player tells the leader their desired starting coordinates, the leader answers the character’s fate (empty cell, river, pit-1) and then the players take their turns in turn, pronouncing one of 4 possible options: left / right / up / down, and the leader moves the corresponding players to the chips on the map, taking into account movements along the rivers and teleportation through the pits. You can supplement the rules with internal walls that can be blown up with grenades replenished in special arsenal cells, enter into the game treasure hunt and exit missions, various new objects - like a mirrored cell, when hit by which the leader declares it as an empty cell, but during
where the leader silently moves the player in the direction opposite to the player’s move, etc.
But even the minimal basic rules of the game create enough interest and mission - to find out the map! It is not as simple as it seems at first glance. You can listen to the answers of the leader and other players, accumulating information about the pieces of the card, trying to glue it together. But any mistake on this path is fraught with the fact that the map is “not going”, and in which piece of error it is impossible to find out, and you have to cross out all the available information and start accumulating it again. But when one manages to figure out the map, a qualitatively new sensation appears (at least for me) - instead of random poking around the walls, swimming down the rivers and flying through the pits, when reality in the form of a master’s response constantly breaks your illusions and predictions to pieces and dust. you already begin to suspect him of mistakes, there is a feeling of complete enlightenment, harmony and comprehension of dao - you can make moves meaningfully, knowing their consequences, build on this tactics and strategy of the game (with a mission), and generally experience incomparable pleasure from Replies reality to your ideas about it :)
In general, I strongly recommend trying it - for example, we with the older-middle 9-year-old son love to play it on walks, without any pens and paper, just from memory - the level of the field is 3 * 3, one river has 3 cells and two holes (2 empty cells remain) he already decides with ease in his mind, and 4 * 4 is still hard for him. We in high school comfortably felt on the field 6 * 6 with an adequate set of objects, and the fields 8 * 8 did not master to pass to the end.
Little about bots
At the end of the article there is a link to the main page of the application that launches and maintains bots. She has a very spartan design, because I have never done web development, much less frontend. But it doesn’t require much - just a brief description, several links, and most importantly - launching an application that falls asleep, if you don’t go to this page for half an hour - so the heroku, where the application is published, saves a limited set of application hours at a free rate.
For each room / personal correspondence, a separate bot session is created with its own namespace, which can change during the request / response process - the standard REPL format (read-eval-print loop). When an application goes to sleep, all user information is erased, and when you wake up, sessions are created anew and a standard library is loaded into each one. Within each room, the global namespace is common to all users, but each user’s command is launched in a separate thread. There are no restrictions on the execution time of commands, but the user cannot start a new command stream until the previous one is completed. To force an interruption of the current stream, use the bot command
! . This allows all channel participants to have access to the overall mutable state, and at the same time to start cyclic processes inside the lambda with their local state.
Separately, I want to mention one problem that arose during the implementation of the chat bot interface. If, when calculating with the command print, output the result to the chat immediately, then it is possible to write spam bombs with infinitely looped output cluttering up the general chat. Therefore, it was decided to print "on the table" - in a separate variable to accumulate all the printing results, and at the end of the calculations output them along with the result of the calculation. But then it becomes impossible to start an interactive cyclic process, in which the bot writes an intermediate output to the chat without finishing the calculation, waiting for input from the user in blocking mode. As a result, I came up with the following option, which absolutely suits me in all parameters - the function blocking the calculation of input from the user read also now can print - but unlike print, it prints not “to the table”, but immediately to the chat window, providing interactivity of interaction . But the spam bomb does not work, because after printing, read waits for the user to enter the blocking mode, so even if there is an endless loop of text, there will be no text without user confirmation.
About the implementation of the game
The entire game code consists of two functions - generating the playing field, and starting the game on the generated field. The text of the functions is quite voluminous, and for example in Telegram I could not download it with one message - the message was split into 2, the bot responded to them separately, of course the syntactic and semantic integrity of the code was lost. But the solution is simple - download each function as a separate message :)
Of course, this does not apply to IRC, where strong restrictions (maximum 512 characters and the absence of multi-line messages) do not allow the bot to load any nontrivial pieces of code. But in the other three listed messengers everything works - and you can see the results in the starting picture of the article. Actually, after loading functions into the REPL, the start of the game may look like this:
join (new-field 5 5 3 4 2) 2 3
This means running the game on a 5 * 5 field inaccessible to anyone else with 3 rivers 4, 2 wells long and a starting row 2 row / 3 column cell. Or so:
def common-field (new-field 5 5 3 4 2)
followed by a call
join common-field 2 3
any number of users, each with their own starting coordinates - everyone will walk in one common field. Of course, you can create another field in the global namespace with a different variable name, and connect to it.
The description of the control commands is displayed in the chat when the game starts. The texts of the functions are listed below:
Code for generating a new field ; - - , , , - ; (defn new-field (max-r max-c rivers-count river-length holes-count) ; 0 - (n-1) ; (def random-int-object (java (class "java.util.Random") "new")) (defmacro random-int (n) java random-int-object "nextInt" n) ; : (1 2 3 4 5) -> 2 ; (defn list-rand (l) cond (null? l) nil (list-ref (random-int (length l)) l)) ; : (1 2 3 4 5) -> (2 (1 3 4 5)) ; (defn get-rand-cell (l) (def c (list-rand l)) (cond (null? l) nil (cons c (filter (lambda (x) not (eq? xc)) l) nil) )) ; , / ; (defn get-free-neighbours (p free-cs) (defn good (p) and (and (<= 1 (car p) max-r) (<= 1 (cadr p) max-c)) (elem p free-cs)) (def neighbours (map (lambda (x) zipwith + px) '((0 -1) (0 1) (-1 0) (1 0)) )) (filter good neighbours) ) ; , : ; ; ((7 3) (1 2 4 5 6)) -> ((4 7 3) (1 2 5 6)) ; (defn get-next-river-cell (river-free-cs) (def river (car river-free-cs) free-cs (cadr river-free-cs)) (def cs (cond (null? river) free-cs (get-free-neighbours (car river) free-cs))) (cond (null? cs) nil ((def c (list-rand cs)) (cons (cons c river) (filter (lambda (x) not (eq? xc)) free-cs) nil)) )) ; : (() (1 2 3 4 5 6 7)) -> ((1 4 7 3) (2 5 6)) ; (defn get-river (len river-free-cs) cond (= 0 len) river-free-cs (null? state) nil (get-river (- len 1) (get-next-river-cell river-free-cs))) ; ; (defn try-get-river (trys len river-free-cs) (def river (get-river len river-free-cs)) (cond (= 0 trys) nil (null? river) (try-get-river (- trys 1) len river-free-cs) river) ) ; , ; (defn add-river (rivers-free-cs) (def rivers (car rivers-free-cs) free-cs (cadr rivers-free-cs)) (def river (try-get-river 50 river-length (cons nil free-cs nil))) (cond (null? river) nil (cons (cons (car river) rivers) (cadr river) nil) )) ; , ; (defn add-hole (holes-free-cs) (def holes (car holes-free-cs) free-cs (cadr holes-free-cs)) (cond (null? (cdr free-cs)) nil ((def a (get-rand-cell free-cs) b (get-rand-cell (cadr a))) (def hole (cons (car a) (car b) nil)) (cons (cons hole holes) (cadr b) nil) ))) (def all-cells (concat (map (lambda (r) map (lambda (c) cons rc) (list-from-to 1 max-c)) (list-from-to 1 max-r) ))) (def rivers-free-cs (ntimes rivers-count add-river (cons nil all-cells nil))) (def holes-free-cs (ntimes holes-count add-hole (cons nil (cadr rivers-free-cs) nil))) (def rivers (car rivers-free-cs) holes (car holes-free-cs)) (cond (or (null? rivers-free-cs) (null? holes-free-cs)) ((print " ") nil) (make '((max-r max-c) rivers holes)) ) )
Start function code ; - ; (defn join (field row col) (match field '((max-r max-c) rivers holes)) ; - ; (defn show-field (cur-p) (def rows (map (lambda (r) map (lambda (c) cons rc) (list-from-to 1 max-c)) (list-from-to 1 max-r) )) (def h-divider (foldl ++ "+" (replicate max-c "+----"))) (def alphabet '("" "A" "B" "C" "D" "E" "F" "G" "H" "I" "J")) (defn show-row (row) foldl (lambda (xa) ++ a (show-point x) (cond (eq? x cur-p) "#" " ") "| ") "| " row) (defn show-point (p) (def rr (get-by-p p rivers (lambda (oi ei) ++ (list-ref oi alphabet) ei))) (def rh (get-by-p p holes (lambda (oi ei) ++ "." oi))) (cond (not (null? rr)) rr (not (null? rh)) rh " ") ) (defn get-by-p (p objects v) (defn go (li) (def ei (+ 1 (elem-index p (car l)) )) (cond (null? l) nil (> ei 0) (vi ei) (go (cdr l) (+ 1 i)) )) (go objects 1)) (foldl (lambda (xa) ++ a \n (show-row x) \n h-divider) h-divider rows) ) ; , ; (defn elem-index (el) (defn go (li) cond (null? l) -1 (eq? e (car l)) i (go (cdr l) (+ 1 i))) (go l 0)) (defn last (l) cond (null? (cdr l)) (car l) (last (cdr l)) ) ; ; (defn co-hole (p hole) (def a (car hole) b (cadr hole)) (cond (eq? pa) b (eq? pb) ap) ) ; ; (defn user-input (p show-flag comment) (def c (cond show-flag (read (show-field p) \n comment) (read comment))) (cond (eq? c 'a) (move p show-flag "" 0 -1) (eq? c 'd) (move p show-flag "" 0 1) (eq? c 'w) (move p show-flag "" -1 0) (eq? c 's) (move p show-flag "" 1 0) (eq? c 'show) (user-input p (not show-flag) "") (eq? c 'quit) " " (user-input p show-flag " ") )) ; ; (defn move (p-pred show-flag dir dr dc) (def r (+ dr (car p-pred)) c (+ dc (cadr p-pred)) in-field (and (<= 1 r max-r) (<= 1 c max-c)) p (cons rc)) (def rr (get-by-p p rivers (lambda (oi river) cons (last river) ""))) (def rh (get-by-p p holes (lambda (oi hole) cons (co-hole p hole) (++ " " oi)))) (cond (not in-field) (user-input p-pred show-flag (++ dir " - ")) (not (null? rr)) (user-input (car rr) show-flag (++ dir " - " (cadr rr))) (not (null? rh)) (user-input (car rh) show-flag (++ dir " - " (cadr rh))) (user-input p show-flag (++ dir " - ")) )) ; ( ), ; (defn get-by-p (p objects v) (defn go (li) cond (null? l) nil (elem p (car l)) (vi (car l)) (go (cdr l) (+ 1 i)) ) (go objects 1)) ; ; (read "adws - ///, show - / , quit - " \n " - ") (move (cons row col) false "" 0 0) )
Threat This code does not claim protection from incorrect input, although it is not difficult to do. Moreover, you can add an automatic control of the strict order of the moves of the participants, in the order of their joining the common field. You can add everything that only fantasy tells! But I wrote this code for example in a few hours, and did not over-complicate it with logic. The only thing I wanted was to write in the most functional style, without mutable states and variables, deviating from pure
silky OP only in 2 points I / O along the course of calculations and generating random numbers when creating the field - but we can assume that we live in IO monad, and there is no crime :) Although of course you could use the built-in mutable java-collections, ArrayList / Map / HashMap, etc., no one interferes. But on this example, I want to bring a simple idea - that you yourself can change it or even write your own programs or your games, and run them online in chat rooms :)
ZZY starting page of the application that launches bots:
liscript.herokuapp.comAll impressions, advice, opinions, suggestions, etc. You can voice in any of the common home channels of bots in all messengers. Well, except for IRC - there when the last online user leaves, the channel is deleted as such along with the entire message history.