Liscript-REPLボットオンライン





少し前に、SICPの読書に触発されて、厳密なセマンティクスを備えたLispライクな言語のインタープリターの実装を2つ作成し、デスクトップGUI、コンソールインターフェイスを追加し、テトリスなどを作成し、Habrに関する記事をいくつか公開しました。



最近、私は幅広い聴衆がこの言語に精通する機会を追加しました-私は次のメッセンジャーのためにREPLボットを書きました:IRC、Telegram、Slack、Gitter。 ボットは、彼らのために特別に作成されたチャンネルにありますが、ほとんどの場合、他のチャンネルに追加/招待して、個人的に連絡を取ることができます。 この形式により、インタープリターのリアルタイム解釈を伴う、関数型プログラミングの基本に関するオンラインテキストレポートが可能になります。



もちろん、アニメーション付きのグラフィックウィンドウは、アプリケーションのデスクトップバージョンでのみ作成できます。 したがって、言語とREPLの可能性をよりよく明らかにするために、任意の数の人がボットで遊ぶことができるラビリンスゲームのテキスト実装を作成しました。 詳細と歌詞の一部。



ゲームの説明とルール



私が小学生の頃(そして、前世紀の80年代でした)、コンピューターとインターネットは、控えめに言っても、現在ほど広く普及していなかったのです。 したがって、クラスメートと友人は、海の戦い、点など、紙にペンで普通の子供のゲームをしました。 とりわけ、このようなゲームがあり、それをラビリンスと呼びました。



ルールは次のとおりです。ファシリテーターが選ばれ、誰も表示せずにマップを作成し、彼の論文にそれを描きます。 カードはn m個のセルからなる長方形のグリッドで、セルは空の場合があり、セルに穴がある場合があります-テレポートなどのオブジェクト-穴が1つのセルに当たると、プレイヤーが別のセルに移動し、リーダーがプレイヤーに穴の番号を呼び出し、マップ上に川があります-彼らは流れます側面に隣接するセルに(対角的に不可能)、プレイヤーが川に入ると、彼は川の端に移動され、リーダーはプレイヤーに川の番号を示さずに泳ぎの事実を知らせます。 壁に向かって行こうとすると、リーダーがこれについて話します。 マップのパラメーター、川/穴の数、および川の長さはすべてのプレイヤーに知られています。 ゲームの開始時に、各プレイヤーは希望する開始座標をリーダーに通知し、リーダーはキャラクターの運命(空のセル、川、ピット-1)に答え、プレイヤーは順番に動き、4つの可能なオプションの1つを発音します:左/右/上/下、リーダーはマップ上のプレイヤーに対応するチップを動かし、川沿いの動きとピットを通るテレポーテーションを考慮します。 グレネードで爆破したり、特別な兵器セルで補充したり、宝物を探し出してゲームミッションに入ったり、さまざまな新しいオブジェクト(ミラーセル、ヒットしたとき、プレゼンターが空のセルとして宣言したり)することができる内部壁でルールを補足できますリーダーがプレイヤーをプレイヤーの動きの反対側に静かに移動させるなど。



しかし、ゲームの最低限の基本的なルールでさえ、十分な関心と使命を生み出します-マップを見つけるために! 一見すると単純ではありません。 あなたはリーダー自身と他のプレイヤーの答えを聞いて、カードの断片に関する情報を蓄積し、それを単一の全体に接着しようとすることができます。 しかし、途中の間違いは、カードが「行きません」という事実と、どの部分でエラーをまだ明確にできないという事実に満ちています-そして、あなたはすべての利用可能な情報を取り消して、それを再び蓄積し始めなければなりません。 しかし、マップを理解しようとすると、(少なくとも私にとっては)質的に新しい感覚が現れます-壁からランダムに突っ込んだり、川で泳いだり、穴を飛んだりする代わりに、ホストからの回答という形の現実が常にあなたの幻想や断片への予測を破りますあなたはすでに彼の間違いを疑い始め、タオの完全な啓発、調和、理解の感覚があります-あなたは意味のある動きをし、その結果を知って、この戦術とゲームの戦略(ミッションがある場合)を構築し、一般的に比類のない喜びを経験することができます それについてのあなたの考えと現実の対応:)



一般的に、私はそれを試すことを強くお勧めします-例えば、私たちは年長の中年の9歳の息子と一緒に、ペンや紙なしで、記憶からだけで散歩するのが大好きです-フィールドレベルは3 * 3、3つのセルに1つの川と2つの穴(空のセルが2つ残っています)彼はすでに心の中で簡単に決定しており、4 * 4はまだ彼にとって難しいです。 高校では、オブジェクトの適切なセットを持つ6 * 6フィールドで快適に感じ、8 * 8フィールドは最後まで通過できませんでした。



ボットについて少し



記事の最後に、ボットを起動および管理するアプリケーションのメインページへのリンクがあります。 彼女は非常に質素なデザインをしています ウェブ開発、特にフロントエンドを行ったことはありません。 ただし、簡単な説明、いくつかのリンク、そして最も重要なことですが、このページに30分アクセスしないと眠るアプリケーションを起動する必要はありません。したがって、アプリケーションが公開されているherokuは、無料の関税で限られたアプリケーション時間を節約します。



部屋/個人の通信ごとに、独自の名前空間を持つ個別のボットセッションが作成されます。これは、要求/応答プロセス中に変更できます-標準REPL形式(読み取り-評価-印刷ループ)。 アプリケーションがスリープ状態になると、すべてのユーザー情報が消去され、目覚めるとセッションが再作成され、それぞれに標準ライブラリがロードされます。 各ルーム内では、グローバル名前空間はすべてのユーザーに共通ですが、各ユーザーのチームは別々のスレッドで実行されます。 コマンドの実行に時間制限はありませんが、ユーザーは前のコマンドが完了するまで新しいスレッドコマンドを開始できません。 現在のスレッドを強制的に中断するには、botコマンドを使用します 。 これにより、すべてのチャネル参加者が共通の可変状態にアクセスでき、同時にローカル状態でラムダ内の循環プロセスを開始できます。



それとは別に、チャットボットインターフェイスの実装中に発生した1つの問題に注意したいと思います。 printコマンドで計算するときに、結果をすぐにチャットに出力すると、無限ループの出力でスパムボムを書き、一般的なチャットを混乱させる可能性があります。 そのため、「テーブルに」印刷することを決定しました-すべての印刷結果を蓄積するために別の変数に、計算の最後にそれらを計算結果と一緒に表示します。 しかし、その後、ボットが計算を完了せず、チャットに中間出力を書き込み、ブロッキングモードのユーザーからの入力を待機するインタラクティブな循環プロセスを開始する機会はなくなります。 その結果、次のオプションを思い付きました。これはあらゆる点で私に完全に合っています-ユーザーの入力計算をブロックする機能は、今読んだものも印刷する方法を知っています-しかし、印刷とは異なり、テーブルに直接ではなく、チャットウィンドウに直接印刷して、対話性を確保します。 また、スパム爆弾は機能しません。読み取りを印刷した後、ユーザーがブロッキングモードに入るまで待機するため、無限のフットバッグをループする場合でも、ユーザーの確認なしにテキストが表示されないためです。



ゲームの実装について



ゲームコード全体は、プレイフィールドの生成と、生成されたフィールドでのゲームの開始という2つの機能で構成されています。 関数のテキストは非常に膨大であり、たとえば、Telegramでは1つのメッセージを読み込むことができませんでした-メッセージは2つに分割され、ボットはそれらに個別に反応しました。もちろん、コードの構文的および意味的整合性は失われました。 しかし、解決策は簡単です-各関数を個別のメッセージにロードしてください:)



もちろん、これはIRCには適用されません。IRCでは、強力な制限(最大512文字と複数行メッセージの不在)により、ボットが重要なコードを読み込むことができません。 しかし、リストされている他の3つのメッセンジャーでは、すべてが機能します。結果は記事の最初の写真で見ることができます。 実際、REPLに関数をロードすると、ゲームの開始は次のようになります。



join (new-field 5 5 3 4 2) 2 3
      
      





これは、3つの川4、長さ2の穴、開始セル2行/ 3列を持つ他の誰にもアクセスできない5 * 5フィールドでゲームを開始することを意味します。 または:



 def common-field (new-field 5 5 3 4 2)
      
      





呼び出しが続きます



 join common-field 2 3
      
      





それぞれが独自の開始座標を持つ任意の数のユーザー-誰もが共通のフィールドを歩きます。 もちろん、グローバル名前空間に異なる変数名を持つ別のフィールドを作成し、それに接続できます。



制御コマンドの説明は、ゲームの開始時にチャットに表示されます。 機能テキストは以下のとおりです。



新しいフィールド生成機能コード
 ;   -  - , , ,    -  ; (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)) ) )
      
      





ゲーム開始機能コード
 ;   -        ; (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) )
      
      





このコードは、不正な入力から保護されているとは主張していませんが、これを行うことはまったく難しくありません。 さらに、参加者の移動の厳密な順序の自動制御を、共通フィールドに参加する順序で追加できます。 ファンタジーだけが伝えるすべてを追加できます! しかし、私はこのコードをほんの一例として数時間で書きましたが、ロジックでやり過ぎたわけではありません。 私がやりたかった唯一のことは、計算中の入力/出力の2つのインスタンスとフィールド作成時の乱数生成でのみ、純粋な絹のような FPから逸脱した、可変状態と変数なしで最も機能的なスタイルで書くことでしたが、私たちは生きていると仮定できますIOモナドでは、犯罪はありません:)もちろん、組み込みの可変JavaコレクションであるArrayList / Map / HashMapなどを使用できますが、誰も気にしません。 しかし、この例では、あなた自身がそれを変更したり、プログラムやゲームを書いたり、チャットルームでオンラインで実行したりできるという簡単な考えを伝えたいと思います。



ZZYボットがアプリケーション開始ページを起動: liscript.herokuapp.com



すべての印象、ヒント、意見、提案など。 すべてのメッセンジャーのボットの一般的なホームチャネルのいずれかで発声できます。 さて、IRCを除きます-最後のオンラインユーザーが去ると、メッセージ履歴全体とともにチャネルが削除されます。



All Articles