Liscript - REPL bots online

Some time ago, inspired by reading SICP, I wrote a couple of my implementations of interpreters of a lisp-like language 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. Bots are located on channels specially created for them, but in most cases they can be added / invited to other channels and have personal correspondence with them. This format allows online text reports on the basics of functional programming, accompanied by a real-time interpretation of the interpreter.
Of course, graphic windows with animation can only be created in the desktop version of the application. Therefore, to better reveal the possibilities of language and REPL, I wrote a textual implementation of the Labyrinth game, 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 it was 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, my classmates and friends 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: a facilitator is selected who makes a map and draws it on his paper, without showing anyone. The card is a rectangular grid of n m cells, the cell may be empty, there may be a hole in the cell - an object like a teleport - when a pit hits one cell, the player moves to another and the presenter tells the player the number of the hole, there are rivers on the map - they only flow in cells adjacent to the sides (diagonally impossible), if a player enters a river, he is transferred to the end of the river and the leader informs the player of the swim without indicating the river number. When you try to go towards the wall, the leader talks about this. Map parameters, number of rivers / holes and river length are known to all players. At the beginning of the game, each player informs the leader of their desired starting coordinates, the leader answers the fate of the character (empty cell, river, pit-1) and then the players make their moves in turn, pronouncing one of 4 possible options: left / right / up / down, and the leader moves the chips corresponding to the players on the map, taking into account movements along the rivers and teleportations through the pits. You can supplement the rules of the internal walls, which can blow up grenades that can be completed in special cells arsenals, to enter into the game the search mission treasure and output, a variety of new objects - mirror the type of cell in contact with which the presenter announces it as an empty cell, but during a whichthe leader silently moves the player to the side opposite to the player’s move, etc.
But even the minimum basic rules of the game create enough interest and mission - to find out a map! It is not as simple as it seems at first glance. You can listen to the answers of the leader yourself and other players, accumulating information about the pieces of the card, trying to glue it into a single whole. But any mistake along the way is fraught with the fact that the card is "not going to", and in what piece the error cannot already be clarified - and you have to cross out all the available information and begin to accumulate it again. But when you manage to figure out a map, a qualitatively new sensation arises (at least for me) - instead of random poking on the walls, swimming in rivers and flying through holes, when reality in the form of the host’s response constantly breaks your illusions and predictions to pieces, and you already begin to suspect him of mistakes, there is a feeling of complete enlightenment,
In general, I highly recommend trying it - for example, we with my older, middle-aged 9-year-old son love to play it on walks, without any pens and paper, just from memory - a field level of 3 * 3, one river in 3 cells and two holes (2 empty cells remain) he already decides with ease in his mind, and 4 * 4 is still difficult for him. In high school, we felt comfortable on a 6 * 6 field with an adequate set of objects, and 8 * 8 fields were not able to go through to the end.
A bit 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, as I have never done web development, especially frontend. But it does not require much - a brief description, several links, and most importantly - launch an application that falls asleep if you don’t go to this page for half an hour - so heroku, where the application is published, saves a limited set of application hours at a free tariff.
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 the application falls asleep, all user information is erased, and upon waking up, sessions are re-created and a standard library is loaded into each. Inside each room, the global namespace is common to all users, but each user’s team runs in a separate thread. There is no time limit for the execution of commands, but the user cannot start a new thread command until the previous one is completed. To force interrupt the current thread, use the bot command !. This allows all channel participants to have access to a common mutable state, and at the same time to start cyclic processes inside lambdas with their local state.
Separately, I want to note one problem that arose during the implementation of the chatbot interface. If, when calculating with the print command, output the result to chat immediately, it is possible to write spam bombs with an infinitely looped output cluttering up the general chat. Therefore, it was decided to print "on the table" - in a separate variable to accumulate all print results, and at the end of the calculations to display them together with the calculation result. But then the opportunity to start an interactive cyclic process, in which the bot does not finish the calculation, writes intermediate output to the chat, waiting for input from the user in blocking mode, disappears. As a result, I came up with the following option, which completely suits me in all respects - the function blocking the calculation of input from the user read now also knows how to print - but unlike print, it prints not "to the table", and immediately into the chat window, providing interactivity. And the spam bomb does not work, because after printing read waits for the user to enter in blocking mode, so even with endless looping, there will be no text without user confirmation.
About the implementation of the game
The entire game code consists of two functions - the generation of the playing field, and the start of the game on the generated field. The text of the functions is quite voluminous, and for example, in Telegram, I couldn’t download it with one message - the message broke into 2, the bot reacted to them separately, of course, the syntactic and semantic integrity of the code was lost. But the solution is simple - load each function with 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 non-trivial 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 the functions in the REPL, the beginning of the game may look like this:
join (new-field 5 5 3 4 2) 2 3
This means - start the game on a 5 * 5 field inaccessible to anyone else with 3 rivers 4, 2 holes long and a starting cell 2 row / 3 column. 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 on one common field. Of course, you can create another field with a different variable name in the global namespace, and connect to it.
The description of the control commands is displayed in the chat at the start of the game. The function texts are listed below:
New field generation function code
; генератор поля - передаем кол-во строк, столбцов, рек, длину рек и кол-во ям ;
(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? x c)) 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 + p x) '((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? x c)) 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 r c) (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)) )
)
Game 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 r c) (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 (x a) ++ 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 (l i)
(def ei (+ 1 (elem-index p (car l)) ))
(cond (null? l) nil (> ei 0) (v i ei) (go (cdr l) (+ 1 i)) ))
(go objects 1))
(foldl (lambda (x a) ++ a \n (show-row x) \n h-divider) h-divider rows) )
; пара тривиальных функций, которым место в стандартной библиотеке ;
(defn elem-index (e l)
(defn go (l i) 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? p a) b (eq? p b) a p) )
; обработка команд пользовательского ввода ;
(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 r c))
(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 (l i) cond (null? l) nil (elem p (car l)) (v i (car l)) (go (cdr l) (+ 1 i)) )
(go objects 1))
; собственно вызов цикла пользовательского ввода с указанной стартовой точки ;
(read "a d w s - влево/вправо/вверх/вниз, show - показывать/скрывать карту, quit - выход" \n "введите что-нибудь для начала игры")
(move (cons row col) false "старт" 0 0)
)
Threat this code does not claim to be protected from incorrect input, although this is not at all difficult to do. Moreover, you can add automatic control of the strict order of participants' moves, in the order in which they join the common field. You can add everything that only fantasy tells! But I wrote this code for an example in a few hours, and did not overdo it with logic. The only thing I wanted to do was write in the most functional style, without mutable states and variables, deviating from pure
ZZY is the application start page that launches bots: liscript.herokuapp.com
All impressions, tips , opinions, wishes, etc. can voice in any of the general 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.