Liscript — REPL bots online
Some time ago, inspired by reading SICP, I've written a few implementations of interpreters listopadovogo language with strict semantics, added desktop GUI, command-line interface, I wrote a Tetris and a lot more, and published a couple of articles on Habr about it.
I recently added the opportunity for a broader audience to become acquainted with the language — wrote REPL bots for the next messengerof: IRC, Telegram, Slack, Gitter. Bots are placed on a specially created channel, but in most cases they can add/invite to other channels and carry with them personal correspondence. This format allows for live text reports on the topic of the foundations of functional programming, followed by a demonstration of the interpreter in real time.
Of course, the graphics window with animation can only be created in the desktop version of the application. Therefore, more disclosure language and Repla I wrote a text implementation of Maze game that can play with the bot any number of people. Details and a bit of lyrics under the cut.
the
Description and rules of the game
When I was a student (and it was 80-ies of the last century), computers and the Internet were, to put it mildly, is not as widely available as it is now. So we with my classmates played the normal childhood games with a pen on a sheet of paper — battleship, dots, etc., among others, it was a game that we called the Maze.
The rules are: choose an instructor who makes the card and draws it on a piece of paper, no one is showing. The map is a rectangular grid of n by m cells, a cell can be empty, the cell can be a pit — type object teleport — when injected into one cell of the pit the player moves to another host and calls the player a number of holes on the map there is a river — they flow only to the adjacent sides of the squares (not diagonal) when you hit the river, the player moved to the end of the river and the presenter tells the player the fact swim without specifying a river. When you try to go to the side of the wall leading talking about it. The map parameters, the number of rivers/wells and the length of rivers, all players known. At the beginning of the game each player reports leading to the desired starting coordinates, the presenter meets the fate of the character (empty square, river, pit-1) and then the players make their moves in turns, saying one of 4 possible options: left/right/up/down, and moves corresponding to the leading players of the chips on the map with the movement of the rivers and teleports to pits. You can Supplement the rules of internal walls that can be blown up with grenades, replenish special cells, the arsenals, to enter the game mission to find the treasure and exit, a variety of new objects — like mirror cells, in contact with which the leader declares it as an empty cell, but if the course that leading silence moves the player towards the opposing player, etc.
But even the minimum basic rules of the game create enough interest and a mission — to learn the map! It's not as easy as it seems at first glance. You can listen to the answers leading yourself and other players by accumulating information about pieces of card, trying to glue it together. But any mistake in this way is fraught with the fact that the card is "not going", and a piece of error already you can not find and have to cross out all of the available information and to start accumulating it again. But when I manage to figure out the map, it shows (at least me) a brand new feeling instead of random poking on the walls, swim through rivers and flying over the wells, when the real answer is lead constantly breaks your illusions and projections in the dust, and you begin to suspect him of error, there is a feeling of complete enlightenment, harmony and the attainment of the Dao to make moves intelligently, knowing their consequences, to build on this strategy and tactics games (with the mission), and generally experience the incomparable pleasure from the satisfactions of reality, your views on it :)
In General we strongly recommend to try — for example, over an average 9-year-old son love to play it on walks, without pens and paper, just at the memory — level fields 3*3, one river of 3 cells and two of the pits (left with 2 empty cells), he solves with ease in mind, and 4*4 he is still hard. We're in high school feel comfortable in the field 6*6 with the appropriate set of objects, and the fields 8*8 was not able to pass through.
the
a Little about bots
At the end of the article there is a link on the main page of the application, launching and maintenance bots. She has a very Spartan design, as I've never done web development, especially front end. But it does not take much — a fairly short description, multiple references, and most importantly — run the app that goes to sleep if not half an hour go to this page — so heroku, where a published application, it saves a limited set of apps on free rate.
For each room/personal correspondence creates a separate session of the bot with its namespace, which can change during the request/response standard format REPL (read-eval-print loop). When falling asleep the application, all user information is erased, and when the revival is the newly created session and each loads a standard library. Within each room space global name common for all users, but the command each user runs in a separate thread. Limitations on the instruction execution time, but the user cannot start a new team thread until the previous one has completed. For forced interruption of current flow command bot !. This allows all channel participants to have access to shared mutable state, and at the same time to start cyclic processes inside the lambdas with your local state.
Separately I want to note one problem encountered in the implementation of the chat-bot interface. If the evaluation of the print command output in chat the result immediately, it is possible to write spam bombs endlessly fixated output, cluttering up General chat. It was therefore decided to print "on the table" in a separate variable to accumulate all the print results, and at the end of calculations to bring them together with the result of the computation. But then it lost the ability to run interactive cyclical process in which the bot not finishing the computation writes in the chat intermediate output, waiting for user input in a blocking mode. As a result, I came up with the following option, which I one hundred percent satisfied in all respects — function blocks the calculation of the input from the user read is also now able to print — but unlike print, it prints not "on the table", and once in the chat window, providing the benefits of interactivity. And spam-bomb doesn't work because after printing the read waits for user input in a blocking mode, so even when looping endless footcloths text without user confirmation will not be.
the
About the game
The entire code consists of two functions — generating playing field, and the start of the game to the generated field. The text of functions is quite extensive, and for example in the Telegram I failed to upload one message — the message is split into 2, the bot will respond to them separately of course syntactic and semantic integrity of the code is lost. But the solution is simple: load each feature in a separate post :)
Of course, this does not apply to IRC, where strong restrictions (maximum of 512 characters and the lack of multi-line messages) does not allow you to load the bot of any non-trivial pieces of code. But in the other three listed messenger everything works — and you can see the results in the starting picture of the article. In fact, after loading the functions in the REPL, start the game might look like this:
the
join (new-field 5 5 3 4 2) 2 3
the
def common-field (new-field 5 5 3 4 2)
with a subsequent call to
the
join common-field 2 3
any number of users, each with their starting coordinates will all go to one General field. Of course, you can create in the global namespace another field with a different variable name, and connect to it.
The description of the command appears in chat at the beginning of the game. The texts of the functions given below:
Code generation function of the new field
; the field generator is pass the number of rows, columns, rivers length of rivers and number of pits ;
(defn new-field (max-r max-c rivers-river count-length holes-count)
; the random number generator in given range 0 - (n-1) ;
(def random-int-object (java (class "java.util.Random") "new"))
(defmacro random-int (n) java random-int-object a "nextInt" n)
take a random element of the list: (1 2 3 4 5) -> 2 ;
(defn list-rand (l) cond (null? l) nil (list-ref (random-int (length l)) l))
; to split a random element from the list: (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) ))
; give free cells of the field, neighboring this horizontal/vertical ;
(defn get-free-neighbours (p free-cs)
(defn good (p) and (and (<= 1 (car p) max-r) (<= 1 (cadr p) max-c)) (free p elem-cs))
(def neighbours (map (lambda (x) zipwith + p x) '((0 -1) (0 1) (-1 0) (1 0)) ))
(filter good neighbours) )
add another cell to the river, Tsepov its free: ;
; ((7 3) (1 2 4 5 6)) -> ((4 7 3) (1 2 5 6)) ;
(defn get-next-river-cell (river-free-cs)
(def river (river car-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)) ))
; dial a river of a predetermined length: (() (1 2 3 4 5 6 7)) -> ((1 4 7 3) (2 5 6)) ;
(defn get-river (river len-free-cs)
cond (= len 0) river-free-cs
(null? state) nil
(get-river (- len 1) (get-next-river-river cell-free-cs)))
; try to gain the river of a predetermined length by limiting the number of unsuccessful attempts ;
(defn try-get-river (trys river len-free-cs)
(def river (get-river river len-free-cs))
(cond (= 0 trys) nil (null? river) (try-get-river (- trys 1) river len-free-cs) river) )
add another river to the list of rivers, reducing the list of free cells ;
(defn add-river (rivers-free-cs)
(def rivers (rivers car-free-cs) free-cs (cadr rivers-free-cs))
(def river (try-get-river river 50-length (cons nil free-cs nil)))
(cond (null? river) nil (cons (cons (car river) rivers) (cadr river) nil) ))
; add the next hole to the list of the pits, reducing the list of free cells ;
(defn add-hole (holes-free-cs)
(def holes (holes car-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 max 1-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 (rivers car-free-cs) holes (holes car-free-cs))
(cond (or (null? rivers-free-cs) (null? holes-free-cs))
((print "failed to create map") nil)
(make '((max-r max-c) rivers holes)) )
)
function Code start of the game
; start the game - hand field and the coordinates of the starting point ;
(defn join (field row col)
(match field '((max-r max-c) rivers holes))
; the line is a beautiful representation of the field indicating the current position of the player ;
(defn show-field (cur-p)
(def rows (map
(lambda (r) map (lambda (c) cons r c) (list-from-to max 1-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 alphabet oi) 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 v objects)
(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) )
; a couple of trivial functions, which in the standard library ;
(defn elem-index (e l)
(defn go (l i) cond (null? l) -1 (eq? e (car l)) i (go (cdr l) (+ i 1)))
(go l 0))
(defn last (l) cond (null? (cdr l)) (car l) (last (cdr l)) )
; obtaining second coordinates of the pit for the first ;
(defn co-hole (p-hole)
(def a (car hole) b (cadr hole)) (cond (eq? p a) b (eq? p b) a p) )
; command processing user input ;
(defn user-input (p show-flag comment)
(def c (cond show-flag (read (show-field p) \n comment) (read comment)))
(eq? c 'd) (move p show-flag "to the right" 0 1)
(eq? c 'w) (move p show-flag up -1 0)
(eq? c 's) (show-move p-flag down 1 0)
(eq? c 'show) (user-input p (not show-flag) "")
(eq? c 'quit) "aborted game"
(user-input p show-flag "invalid command") ))
; move player in that direction and again the call for user input ;
(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) "river")))
(def rh (get-by-p p holes (lambda (oi hole) cons (co-hole p hole) (++ "pit" oi))))
(cond (not in-field) (user-input p-pred show-flag (dir ++ "- wall"))
(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 ++ "empty")) ))
search the passed position in the list of objects (rivers or pits) returns applied visitor ;
(defn get-by-p (p v objects)
(defn go (l i) cond (null? l) nil (elem p (car l)) (v i (car l)) (go (cdr l) (+ i 1)) )
(go objects 1))
; the actual loop call, user input from a specified starting point ;
(read "a d w s left/right/up/down, show - show/hide map quit - exit \n enter anything to start the game")
(move (cons row col) false "start" 0 0)
)
PS this code does not claim to protect from invalid input, although it is quite easy to do. Moreover, you can add automatic control of a strict order of moves of the participants, in order of their accession to the common field. You can add all, only the imagination will tell! But this code I wrote for example over several hours, and did not perellonet his logic. The only thing I want is to write in highly functional style with no mutable state and variables, deviating from the pure
ZZY opening page of the application that run bots: liscript.herokuapp.com
All experiences, advice, opinions, suggestions, etc. can voice in any of the common home channels bots all messenger. Well, except for IRC there is the release of the latest online user channel is removed as such, along with the entire message history.
Комментарии
Отправить комментарий