;;; Guess an Animal - CL implementation.
;;;
;;; Copyright (C) 1997-2000 by Sam Steingold.
;;; This is Free Software, covered by the GNU GPL (v2)
;;; See http://www.gnu.org/copyleft/gpl.html
;;;
;;; $Id: animals.lisp,v 2.10 2001/11/02 22:31:15 sds Exp $
;;; $Source: /cvsroot/clocc/clocc/src/cllib/animals.lisp,v $

(eval-when (compile load eval)
  (require :cllib-base (translate-logical-pathname "clocc:src;cllib;base"))
  ;; `string-beg-with'
  (require :cllib-string (translate-logical-pathname "cllib:string"))
  ;; `alist->hash-table', `hash-table->alist'
  (require :cllib-miscprint (translate-logical-pathname "cllib:miscprint"))
  ;; `write-to-file', `save-restore'
  (require :cllib-fileio (translate-logical-pathname "cllib:fileio"))
  ;; `+clos-readtable+'
  (require :cllib-closio (translate-logical-pathname "cllib:closio"))
  ;; `mesg'
  (require :cllib-log (translate-logical-pathname "cllib:log"))
  ;; `symbol-concat'
  (require :cllib-symb (translate-logical-pathname "cllib:symb")))

(in-package :cllib)

(export '(play-animals play-game *animals-file-name*))

;;;
;;; Standard version
;;;

(defvar *animals-debug-use-built-in-data* nil "Do not read the file.")
(defvar *animals-default-data*
  '("Is it an insect" ("Can it sting" "a bee" .
    "a roach") "Can it fly" "a duck" . "a penguin")
  "The built-in to be used if `*animals-debug-use-built-in-data*' is non-nil.")
(defvar *animals-data* nil "The actual data tree.")
(defcustom *animals-file-name* string "animals"
  "*The data file.
Defaults to (merge-pathnames \"animals\" *datadir*).")

(defun anml-get-question (old new)
  "Get the question that will distinguish betwee OLD and NEW.
The question will contain `it'."
  (do ((quest nil (if (search "it" quest) quest nil)))
      ((null quest) quest)
    (setq quest
	  (get-string t "What Yes/No question distinguishes between ~a and ~a?"
		      old new))))

(defun get-string (fmt &rest args)
  "Ask for input, return it."
  (apply #'format t fmt args)
  (read-line t))

(defun anml-add-article (str)
  "Add an article in the beginning of the string."
  (declare (simple-string str))
  (setq str (string-trim +whitespace+ str))
  (if (let ((len (length str)))
        (or (string-beg-with "a " str len)
            (string-beg-with "an " str len)
            (string-beg-with "the " str len)))
      str
      (concatenate 'string (if (find (schar str 0) #(#\a #\e #\i #\o #\u)
                                     :test #'char=)
                               "an " "a ") str)))

(defun anml-chop-article (str)
  "Remove the article from the beginning of the string.
Returnes a fresh string."
  (declare (simple-string str))
  (setq str (string-trim +whitespace+ str))
  (let ((len (length str)))
    (cond ((string-beg-with "a " str len) (subseq str 2))
          ((string-beg-with "an " str len) (subseq str 3))
          ((string-beg-with "the " str len) (subseq str 4))
          (t str))))

(defun fix-question (quest)
  (declare (simple-string quest))
  (let ((qq (string-trim +whitespace+ quest)))
    (declare (simple-string qq))
    (if (char= #\? (schar qq (1- (length qq)))) qq
        (concatenate 'string qq "?"))))

(defun get-question (1st 2nd)
  (loop :for quest :of-type simple-string =
        (fix-question
         (get-string "What Yes/No question distinguishes between ~a and ~a?
 ==> " 1st 2nd))
        :for it-pos :of-type (or null index-t) = (search "it" quest)
        :unless it-pos :do (format t "The question must contain `it'.~%")
        :else :return
        (if (y-or-n-p "~a~a~a" (subseq quest 0 it-pos) 1st
                      (subseq quest (+ it-pos 2)))
            (values quest 1st 2nd) (values quest 2nd 1st))))

(defun anml-finish (tail)
  "Endgame."
  (cond ((y-or-n-p "Is it ~a? " tail)
	 (format t "I won!~%") tail)
	(t
         (let ((new (anml-add-article
                     (get-string "I lost...~%What was your animal?..."))))
           (multiple-value-bind (quest 1st 2nd) (get-question new tail)
             (cons quest (cons 1st 2nd)))))))

(defun save-restore-animals (&optional what)
  (save-restore what :var '*animals-data* :name *animals-file-name*))

;;;###autoload
(defun play-animals ()
  "Play the famous game!"
  ;; read the initial data
  (if *animals-debug-use-built-in-data*
      (setq *animals-data* *animals-default-data*)
      (save-restore-animals))
  (mesg :animals t "animals-data now:~%~s~%" *animals-data*)
  ;; play the game
  (let ((animals-data-modified nil))
    (do ((root *animals-data* *animals-data*) follow over)
        (over)
      (do () ((not (consp root)))
        (setq follow root
              root (if (y-or-n-p (car root)) (cadr root) (cddr root))))
      (setf (cdr (or follow *animals-data*))
            (if (eq root (cadr follow))
                (cons (anml-finish root) (cddr follow))
                (cons (cadr follow) (anml-finish root)))
            animals-data-modified t)
      (mesg :animals t "`*animals-data*' now: ~a~%" *animals-data*)
      (setq over (y-or-n-p "Quit?")))
    ;; save the new information
    (mesg :animals t "done - saving the information...~%")
    (if *animals-debug-use-built-in-data*
        (print "used built-in data -- no save!")
        (if animals-data-modified
            (save-restore-animals t)
            (format t "You taught me no new animals this time...~%"))))
  (when (y-or-n-p "Exit lisp?") (quit)))

;;;
;;; Network implementation
;;;

(eval-when (compile load eval)  ; ACL: for `*root-node*'
(defclass node ()
  ((name :type symbol :accessor node-name :initarg name)
   (info :type simple-string :accessor node-info :initarg info)
   (yes :type symbol :accessor node-yes :initarg yes)
   (no :type symbol :accessor node-no :initarg no))
  (:documentation "The information node."))
)

(defmethod print-object ((nd node) (out stream))
  (if *print-readably* (call-next-method)
      (format out "[~s: ~s~@[ y:~s~]~@[ n:~s~]]" (node-name nd) (node-info nd)
              (if (slot-boundp nd 'yes) (node-yes nd))
              (if (slot-boundp nd 'no) (node-no nd)))))

(defcustom *network* hash-table (make-hash-table :test 'eq)
  "*symbol --> node.")

;(defconst +bad-node+ node (make-instance 'node)
;  "*The convenient constant for init.")

(defun name->node (name)
  "Find the node with this name."
  (declare (symbol name))
  (multiple-value-bind (node found-p) (gethash name *network*)
    (assert found-p (name) "No such node: `~s'" name)
    (assert (eq name (node-name node)) (name) "Node (~s) name (~s) mismatch"
            node name)
    node))

(defun mknode (name info &optional yes no)
  "Make a node and add it to the network."
  (assert name (name) "Cannot create a NULL node (~s ~s ~s ~s)"
          name info yes no)
  (let ((new (if yes (make-instance 'node 'name name 'info info
                                    'yes yes 'no no)
                 (make-instance 'node 'name name 'info info))))
    (multiple-value-bind (old found-p) (gethash name *network*)
      (when found-p
        (warn " ** Redefining node `~s' from~% -- ~s~% ** to~% -- ~s~%"
              name old new)))
    (setf (gethash name *network*) new)))

(defcustom *root-node* node
  (mknode 'root "Is is a living being?" 'living 'object)
  "*The root node, from which the search starts by default.")

(defun save-restore-network (&optional what)
  (save-restore what :name "network.dat"
                :var '*network* :basedir *datadir*
                :voidp (lambda (ht) (>= 1 (hash-table-count ht)))
                :readtable +clos-readtable+
                :pre-save #'hash-table->alist
                :post-read #'alist->hash-table))

(defun resolve (&optional (root *root-node*))
  "Walk through the `*network*' starting with root."
  (declare (type (or node symbol) root))
  (when (symbolp root) (setq root (name->node root)))
  (if (slot-boundp root 'yes)
      (let* ((ans (if (y-or-n-p (node-info root))
                      (node-yes root) (node-no root)))
             (new (gethash ans *network*)))
        (if new (resolve new) (values ans root)))
      (values (node-info root) root)))

(defun to-symbol (string)
  "Chop article, upcase and intern."
  (declare (string string))
  (intern (nstring-upcase
           (nsubstitute #\- #\Space (anml-chop-article string)))))

(defun concat-down (&rest args)
  "Concatenate the strings, downcasing."
  (nstring-downcase (apply #'concatenate 'string args)))

(defun get-symbol (&rest args)
  "Intern whatever was returned by `get-string'."
  (to-symbol (apply #'get-string args)))

(defun get-new-symbol (&rest args)
  "Call `get-symbol' and ensure that the symbol is not in `*network*'."
  (loop :for name :of-type symbol = (apply #'get-symbol args)
        :for node :of-type (or null node) = (gethash name *network*)
        :while node :do (format t "The name exists: ~s~%" node)
        :finally (return name)))

(defun add-node (name node)
  "Add a node at the return values of `resolve'."
  (declare (type node node) #+nil(type (or symbol string) name))
  (typecase name
    (symbol                     ; symbol w/o node
     (format t " *** adding a node named `~s'~%" name)
     (if (y-or-n-p
          "  *  Is it a leaf (object) or a node (category)? [Y-leaf; N-node]")
         (mknode name
                 (get-string "What is the content of the `~s' node?" name))
         (mknode name
                 (fix-question
                  (get-string "What is the question for the node `~s'?" name))
                 (get-symbol "What is the `yes' answer?")
                 (get-symbol "What is the `no' answer?"))))
    (string                     ; node w/o y/n
     (let* ((new (get-string "I lost...~%...What was it?"))
            (nna (symbol-concat (node-name node) "*"))
            (nsy (to-symbol new)) (nno (gethash nsy *network*)))
       (when (and nno
                  (not (y-or-n-p
                        " * Hey, I seem to know of ~s: ~s~% * Did you mean it?"
                        nsy nno)))
         (setq nsy (symbol-concat nsy "*") nno (mknode nsy name)))
       (multiple-value-bind (quest yes no) (get-question new name)
         (mknode nna name)
         (setf (node-info node) quest)
         (cond ((string= name yes)
                (setf (node-yes node) nna (node-no node) nsy))
               ((string= name no)
                (setf (node-no node) nna (node-yes node) nsy))
               (t (error 'code :proc 'add-node
                         :args (list quest yes no name node) :mesg
                         "get-question (~s ~s ~s): name: `~s'; node: ~s"))))))
    (t (error 'case-error :proc 'add-node :args
              (list 'name name 'symbol 'string)))))

(defun animals->network (&optional (data *animals-data*) (base "ANIMAL"))
  "Merge the animals data into the network."
  (let* ((nm (get-new-symbol "Content: ~s~%Base: ~s~%* What is the node name? "
                             (car data) base))
         (yy (cadr data)) (nn (cddr data)) (st (symbol-name nm)))
    (mknode nm (car data)
            (if (stringp yy) (node-name (mknode (to-symbol yy) yy))
                (animals->network yy (concat-down base " " st)))
            (if (stringp nn) (node-name (mknode (to-symbol nn) nn))
                (animals->network nn (concat-down base " !" st))))
    nm))

(defun play-game (&optional (root *root-node*))
  "Interactive `resolve'/`add-node'."
  (declare (type (or node symbol) root))
  (save-restore-network)
  (loop :do (multiple-value-bind (name node) (resolve root)
              (declare #+nil(type (or symbol string) name) (type node node))
              (if (typecase name
                    (string (y-or-n-p "Is it ~s?" name))
                    (symbol
                     (not (y-or-n-p "Dangling pointer: `~s'.  Add a node?"
                                    name)))
                    (t (error 'case-error :proc 'play-game :args
                              (list 'name name 'symbol 'string))))
                  (format t "I won!~%")
                  (add-node name node)))
        :while (y-or-n-p "One more game?"))
  (save-restore-network t))

(provide :cllib-animals)
;;; animals.lisp ends here
