*** empty log message ***
[gnus] / lisp / gnus-score.el
index a2616b6..4bdbcb6 100644 (file)
@@ -1,8 +1,8 @@
-1;;; gnus-score.el --- scoring code for Gnus
-;; Copyright (C) 1995,96,97 Free Software Foundation, Inc.
+;;; gnus-score.el --- scoring code for Gnus
+;; Copyright (C) 1995,96,97,98 Free Software Foundation, Inc.
 
 ;; Author: Per Abrahamsen <amanda@iesd.auc.dk>
-;;     Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
+;;     Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;; Keywords: news
 
 ;; This file is part of GNU Emacs.
@@ -32,6 +32,7 @@
 (require 'gnus-sum)
 (require 'gnus-range)
 (require 'message)
+(require 'score-mode)
 
 (defcustom gnus-global-score-files nil
   "List of global score files and directories.
@@ -107,7 +108,11 @@ See the documentation to these functions for more information.
 
 This variable can also be a list of functions to be called.  Each
 function should either return a list of score files, or a list of
-score alists."
+score alists.
+
+If functions other than these pre-defined functions are used,
+the `a' symbolic prefix to the score commands will always use
+\"all.SCORE\"."
   :group 'gnus-score-files
   :type '(radio (function-item gnus-score-find-single)
                (function-item gnus-score-find-hierarchical)
@@ -195,8 +200,8 @@ It can be:
   :type '(choice string
                 (repeat (choice string
                                 (cons regexp (repeat file))
-                                function))
-                function))
+                                (function :value fun)))
+                (function :value fun)))
 
 (defcustom gnus-home-adapt-file nil
   "Variable to control where new adaptive score entries are to go.
@@ -206,8 +211,8 @@ This variable allows the same syntax as `gnus-home-score-file'."
   :type '(choice string
                 (repeat (choice string
                                 (cons regexp (repeat file))
-                                function))
-                function))
+                                (function :value fun)))
+                (function :value fun)))
 
 (defcustom gnus-default-adaptive-score-alist
   '((gnus-kill-file-mark)
@@ -216,7 +221,7 @@ This variable allows the same syntax as `gnus-home-score-file'."
     (gnus-catchup-mark (subject -10))
     (gnus-killed-mark (from -1) (subject -20))
     (gnus-del-mark (from -2) (subject -15)))
-"Alist of marks and scores."
+"*Alist of marks and scores."
 :group 'gnus-score-adapt
 :type '(repeat (cons (symbol :tag "Mark")
                     (repeat (list (choice :tag "Header"
@@ -245,7 +250,7 @@ This variable allows the same syntax as `gnus-home-score-file'."
     "being" "current" "back" "still" "go" "point" "value" "each" "did"
     "both" "true" "off" "say" "another" "state" "might" "under" "start"
     "try" "re")
-  "Default list of words to be ignored when doing adaptive word scoring."
+  "*Default list of words to be ignored when doing adaptive word scoring."
   :group 'gnus-score-adapt
   :type '(repeat string))
 
@@ -254,7 +259,7 @@ This variable allows the same syntax as `gnus-home-score-file'."
     (,gnus-catchup-mark . -10)
     (,gnus-killed-mark . -20)
     (,gnus-del-mark . -15))
-"Alist of marks and scores."
+"*Alist of marks and scores."
 :group 'gnus-score-adapt
 :type '(repeat (cons (character :tag "Mark")
                     (integer :tag "Score"))))
@@ -264,6 +269,11 @@ This variable allows the same syntax as `gnus-home-score-file'."
   :group 'gnus-score-adapt
   :type '(choice (const nil) integer))
 
+(defcustom gnus-adaptive-word-no-group-words nil
+  "If t, don't adaptively score words included in the group name."
+  :group 'gnus-score-adapt
+  :type 'boolean)
+
 (defcustom gnus-score-mimic-keymap nil
   "*Have the score entry functions pretend that they are a keymap."
   :group 'gnus-score-default
@@ -326,7 +336,7 @@ Should be one of the following symbols.
  f: fuzzy string
  r: regexp string
  b: before date
- a: at date
+ a: after date
  n: this date
  <: less than number
  >: greater than number
@@ -339,7 +349,7 @@ If nil, the user will be asked for a match type."
                 (const :tag "fuzzy string" f)
                 (const :tag "regexp string" r)
                 (const :tag "before date" b)
-                (const :tag "at date" a)
+                (const :tag "after date" a)
                 (const :tag "this date" n)
                 (const :tag "less than number" <)
                 (const :tag "greater than number" >)
@@ -372,6 +382,11 @@ If nil, the user will be asked for a duration."
   :group 'gnus-score-files
   :type 'function)
 
+(defcustom gnus-score-thread-simplify nil
+  "If non-nil, subjects will simplified as in threading."
+  :group 'gnus-score-various
+  :type 'boolean) 
+
 \f
 
 ;; Internal variables.
@@ -439,7 +454,6 @@ of the last successful match.")
 
 (gnus-define-keys (gnus-summary-score-map "V" gnus-summary-mode-map)
   "s" gnus-summary-set-score
-  "a" gnus-summary-score-entry
   "S" gnus-summary-current-score
   "c" gnus-score-change-score-file
   "C" gnus-score-customize
@@ -491,7 +505,7 @@ used as score."
            (?l "lines" nil nil number)
            (?d "date" nil nil date)
            (?f "followup" nil nil string)
-           (?T "thread" nil nil string)))
+           (?r "thread" nil nil string)))
         (char-to-type
          '((?s s "substring" string)
            (?e e "exact string" string)
@@ -500,8 +514,8 @@ used as score."
            (?z s "substring" body-string)
            (?p r "regexp string" body-string)
            (?b before "before date" date)
-           (?a at "at date" date)
-           (?n now "this date" date)
+           (?a after "after date" date)
+           (?n at "this date" date)
            (?< < "less than number" number)
            (?> > "greater than number" number)
            (?= = "equal to number" number)))
@@ -628,7 +642,15 @@ used as score."
       (save-excursion
        (set-buffer gnus-summary-buffer)
        (gnus-score-load-file
-        (gnus-score-file-name "all"))))
+        ;; This is a kludge; yes...
+        (cond
+         ((eq gnus-score-find-score-files-function
+              'gnus-score-find-hierarchical)
+          (gnus-score-file-name ""))
+         ((eq gnus-score-find-score-files-function 'gnus-score-find-single)
+          current-score-file)
+         (t
+          (gnus-score-file-name "all"))))))
     
     (gnus-summary-score-entry
      (nth 1 entry)                     ; Header
@@ -649,7 +671,7 @@ used as score."
 (defun gnus-score-insert-help (string alist idx)
   (setq gnus-score-help-winconf (current-window-configuration))
   (save-excursion
-    (set-buffer (get-buffer-create "*Score Help*"))
+    (set-buffer (gnus-get-buffer-create "*Score Help*"))
     (buffer-disable-undo (current-buffer))
     (delete-windows-on (current-buffer))
     (erase-buffer)
@@ -729,20 +751,6 @@ SCORE is the score to add.
 DATE is the expire date, or nil for no expire, or 'now for immediate expire.
 If optional argument `PROMPT' is non-nil, allow user to edit match.
 If optional argument `SILENT' is nil, show effect of score entry."
-  (interactive
-   (list (completing-read "Header: "
-                         gnus-header-index
-                         (lambda (x) (fboundp (nth 2 x)))
-                         t)
-        (read-string "Match: ")
-        (if (y-or-n-p "Use regexp match? ") 'r 's)
-        (and current-prefix-arg
-             (prefix-numeric-value current-prefix-arg))
-        (cond ((not (y-or-n-p "Add to score file? "))
-               'now)
-              ((y-or-n-p "Expire kill? ")
-               (current-time-string))
-              (t nil))))
   ;; Regexp is the default type.
   (when (eq type t)
     (setq type 'r))
@@ -805,7 +813,7 @@ If optional argument `SILENT' is nil, show effect of score entry."
                                  (or (nth 1 new)
                                      gnus-score-interactive-default-score)))
          ;; Nope, we have to add a new elem.
-         (gnus-score-set header (if old (cons new old) (list new))))
+         (gnus-score-set header (if old (cons new old) (list new)) nil t))
        (gnus-score-set 'touched '(t))))
 
     ;; Score the current buffer.
@@ -955,7 +963,7 @@ SCORE is the score to add."
             "references" id 's
             score (current-time-string))))))))
 
-(defun gnus-score-set (symbol value &optional alist)
+(defun gnus-score-set (symbol value &optional alist warn)
   ;; Set SYMBOL to VALUE in ALIST.
   (let* ((alist
          (or alist
@@ -964,7 +972,8 @@ SCORE is the score to add."
         (entry (assoc symbol alist)))
     (cond ((gnus-score-get 'read-only alist)
           ;; This is a read-only score file, so we do nothing.
-          )
+          (when warn
+            (gnus-message 4 "Note: read-only score file; entry discarded")))
          (entry
           (setcdr entry value))
          ((null alist)
@@ -1051,8 +1060,9 @@ SCORE is the score to add."
   ;; Load score file FILE.  Returns a list a retrieved score-alists.
   (let* ((file (expand-file-name
                (or (and (string-match
-                         (concat "^" (expand-file-name
-                                      gnus-kill-files-directory))
+                         (concat "^" (regexp-quote
+                                      (expand-file-name
+                                       gnus-kill-files-directory)))
                          (expand-file-name file))
                         file)
                    (concat (file-name-as-directory gnus-kill-files-directory)
@@ -1079,9 +1089,13 @@ SCORE is the score to add."
          found)
       (while a
        ;; Downcase all header names.
-       (when (stringp (caar a))
+       (cond
+        ((stringp (caar a))
          (setcar (car a) (downcase (caar a)))
          (setq found t))
+        ;; Advanced scoring.
+        ((consp (caar a))
+         (setq found t)))
        (pop a))
       ;; If there are actual scores in the alist, we add it to the
       ;; return value of this function.
@@ -1107,7 +1121,7 @@ SCORE is the score to add."
                 (or (not decay)
                     (gnus-decay-scores alist decay)))
        (gnus-score-set 'touched '(t) alist)
-       (gnus-score-set 'decay (list (gnus-time-to-day (current-time)))))
+       (gnus-score-set 'decay (list (gnus-time-to-day (current-time))) alist))
       ;; We do not respect eval and files atoms from global score
       ;; files.
       (when (and files (not global))
@@ -1121,12 +1135,16 @@ SCORE is the score to add."
       ;; We then expand any exclude-file directives.
       (setq gnus-scores-exclude-files
            (nconc
-            (mapcar
-             (lambda (sfile)
-               (expand-file-name sfile (file-name-directory file)))
-             exclude-files)
+            (apply
+             'nconc
+             (mapcar
+              (lambda (sfile)
+                (list
+                 (expand-file-name sfile (file-name-directory file))
+                 (expand-file-name sfile gnus-kill-files-directory)))
+              exclude-files))
             gnus-scores-exclude-files))
-      (unless local
+      (when local
        (save-excursion
          (set-buffer gnus-summary-buffer)
          (while local
@@ -1195,10 +1213,16 @@ SCORE is the score to add."
                    (read (current-buffer))
                  (error
                   (gnus-error 3.2 "Problem with score file %s" file))))))
-      (if (eq (car alist) 'setq)
-         ;; This is an old-style score file.
-         (setq gnus-score-alist (gnus-score-transform-old-to-new alist))
-       (setq gnus-score-alist alist))
+      (cond
+       ((and alist
+            (atom alist))
+       ;; Bogus score file.
+       (error "Invalid syntax with score file %s" file))
+       ((eq (car alist) 'setq)
+       ;; This is an old-style score file.
+       (setq gnus-score-alist (gnus-score-transform-old-to-new alist)))
+       (t
+       (setq gnus-score-alist alist)))
       ;; Check the syntax of the score file.
       (setq gnus-score-alist
            (gnus-score-check-syntax gnus-score-alist file)))))
@@ -1293,7 +1317,7 @@ SCORE is the score to add."
                (and (file-exists-p file)
                     (not (file-writable-p file))))
            ()
-         (setq score (setcdr entry (delq (assq 'touched score) score)))
+         (setq score (setcdr entry (gnus-delete-alist 'touched score)))
          (erase-buffer)
          (let (emacs-lisp-mode-hook)
            (if (string-match
@@ -1305,7 +1329,8 @@ SCORE is the score to add."
                (gnus-prin1 score)
              ;; This is a normal score file, so we print it very
              ;; prettily.
-             (pp score (current-buffer))))
+             (let ((lisp-mode-syntax-table score-mode-syntax-table))
+               (pp score (current-buffer)))))
          (gnus-make-directory (file-name-directory file))
          ;; If the score file is empty, we delete it.
          (if (zerop (buffer-size))
@@ -1378,7 +1403,7 @@ SCORE is the score to add."
                          gnus-scores-articles))))
 
          (save-excursion
-           (set-buffer (get-buffer-create "*Headers*"))
+           (set-buffer (gnus-get-buffer-create "*Headers*"))
            (buffer-disable-undo (current-buffer))
            (when (gnus-buffer-live-p gnus-summary-buffer)
              (message-clone-locals gnus-summary-buffer))
@@ -1632,7 +1657,7 @@ SCORE is the score to add."
            (setq request-func 'gnus-request-article))
          (while articles
            (setq article (mail-header-number (caar articles)))
-           (gnus-message 7 "Scoring on article %s of %s..." article last)
+           (gnus-message 7 "Scoring article %s of %s..." article last)
            (when (funcall request-func article gnus-newsgroup-name)
              (widen)
              (goto-char (point-min))
@@ -1828,6 +1853,8 @@ SCORE is the score to add."
   ;; Insert the unique article headers in the buffer.
   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
        ;; gnus-score-index is used as a free variable.
+        (simplify (and gnus-score-thread-simplify
+                       (string= "subject" header)))
        alike last this art entries alist articles
        fuzzies arts words kill)
 
@@ -1843,6 +1870,8 @@ SCORE is the score to add."
     (erase-buffer)
     (while (setq art (pop articles))
       (setq this (aref (car art) gnus-score-index))
+      (if simplify
+         (setq this (gnus-map-function gnus-simplify-subject-functions this)))
       (if (equal last this)
          ;; O(N*H) cons-cells used here, where H is the number of
          ;; headers.
@@ -1868,7 +1897,6 @@ SCORE is the score to add."
            entries (assoc header alist))
       (while (cdr entries)             ;First entry is the header index.
        (let* ((kill (cadr entries))
-              (match (nth 0 kill))
               (type (or (nth 3 kill) 's))
               (score (or (nth 1 kill) gnus-score-interactive-default-score))
               (date (nth 2 kill))
@@ -1876,6 +1904,12 @@ SCORE is the score to add."
               (mt (aref (symbol-name type) 0))
               (case-fold-search (not (memq mt '(?R ?S ?E ?F))))
               (dmt (downcase mt))
+                                       ; Assume user already simplified regexp and fuzzies
+              (match (if (and simplify (not (memq dmt '(?f ?r))))
+                          (gnus-map-function
+                           gnus-simplify-subject-functions
+                           (nth 0 kill))
+                        (nth 0 kill)))
               (search-func
                (cond ((= dmt ?r) 're-search-forward)
                      ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
@@ -1884,10 +1918,12 @@ SCORE is the score to add."
          (cond
           ;; Fuzzy matches.  We save these for later.
           ((= dmt ?f)
-           (push (cons entries alist) fuzzies))
+           (push (cons entries alist) fuzzies)
+           (setq entries (cdr entries)))
           ;; Word matches.  Save these for even later.
           ((= dmt ?w)
-           (push (cons entries alist) words))
+           (push (cons entries alist) words)
+           (setq entries (cdr entries)))
           ;; Exact matches.
           ((= dmt ?e)
            ;; Do exact matching.
@@ -1912,7 +1948,26 @@ SCORE is the score to add."
                            gnus-score-trace))
                       (while (setq art (pop arts))
                         (setcdr art (+ score (cdr art)))))))
-             (forward-line 1)))
+             (forward-line 1))
+           ;; Update expiry date
+           (if trace
+               (setq entries (cdr entries))
+             (cond
+              ;; Permanent entry.
+              ((null date)
+               (setq entries (cdr entries)))
+              ;; We have a match, so we update the date.
+              ((and found gnus-update-score-entry-dates)
+               (gnus-score-set 'touched '(t) alist)
+               (setcar (nthcdr 2 kill) now)
+               (setq entries (cdr entries)))
+              ;; This entry has expired, so we remove it.
+              ((and expire (< date expire))
+               (gnus-score-set 'touched '(t) alist)
+               (setcdr entries (cddr entries)))
+              ;; No match; go to next entry.
+              (t
+               (setq entries (cdr entries))))))
           ;; Regexp and substring matching.
           (t
            (goto-char (point-min))
@@ -1931,26 +1986,26 @@ SCORE is the score to add."
                          gnus-score-trace))
                (while (setq art (pop arts))
                  (setcdr art (+ score (cdr art)))))
-             (forward-line 1))))
-         ;; Update expiry date
-         (if trace
-             (setq entries (cdr entries))
-           (cond
-            ;; Permanent entry.
-            ((null date)
-             (setq entries (cdr entries)))
-            ;; We have a match, so we update the date.
-            ((and found gnus-update-score-entry-dates)
-             (gnus-score-set 'touched '(t) alist)
-             (setcar (nthcdr 2 kill) now)
-             (setq entries (cdr entries)))
-            ;; This entry has expired, so we remove it.
-            ((and expire (< date expire))
-             (gnus-score-set 'touched '(t) alist)
-             (setcdr entries (cddr entries)))
-            ;; No match; go to next entry.
-            (t
-             (setq entries (cdr entries))))))))
+             (forward-line 1))
+           ;; Update expiry date
+           (if trace
+               (setq entries (cdr entries))
+             (cond
+              ;; Permanent entry.
+              ((null date)
+               (setq entries (cdr entries)))
+              ;; We have a match, so we update the date.
+              ((and found gnus-update-score-entry-dates)
+               (gnus-score-set 'touched '(t) alist)
+               (setcar (nthcdr 2 kill) now)
+               (setq entries (cdr entries)))
+              ;; This entry has expired, so we remove it.
+              ((and expire (< date expire))
+               (gnus-score-set 'touched '(t) alist)
+               (setcdr entries (cddr entries)))
+              ;; No match; go to next entry.
+              (t
+               (setq entries (cdr entries))))))))))
 
     ;; Find fuzzy matches.
     (when fuzzies
@@ -1982,18 +2037,19 @@ SCORE is the score to add."
                  (setcdr art (+ score (cdr art))))))
            (forward-line 1))
          ;; Update expiry date
-         (cond
-          ;; Permanent.
-          ((null date)
-           )
-          ;; Match, update date.
-          ((and found gnus-update-score-entry-dates)
-           (gnus-score-set 'touched '(t) (cdar fuzzies))
-           (setcar (nthcdr 2 kill) now))
-          ;; Old entry, remove.
-          ((and expire (< date expire))
-           (gnus-score-set 'touched '(t) (cdar fuzzies))
-           (setcdr (caar fuzzies) (cddaar fuzzies))))
+         (if (not trace)
+             (cond
+              ;; Permanent.
+              ((null date)
+               )
+              ;; Match, update date.
+              ((and found gnus-update-score-entry-dates)
+               (gnus-score-set 'touched '(t) (cdar fuzzies))
+               (setcar (nthcdr 2 kill) now))
+              ;; Old entry, remove.
+              ((and expire (< date expire))
+               (gnus-score-set 'touched '(t) (cdar fuzzies))
+               (setcdr (caar fuzzies) (cddaar fuzzies)))))
          (setq fuzzies (cdr fuzzies)))))
 
     (when words
@@ -2019,18 +2075,19 @@ SCORE is the score to add."
                (while (setq art (pop arts))
                  (setcdr art (+ score (cdr art))))))
            ;; Update expiry date
-           (cond
-            ;; Permanent.
-            ((null date)
-             )
-            ;; Match, update date.
-            ((and found gnus-update-score-entry-dates)
-             (gnus-score-set 'touched '(t) (cdar words))
-             (setcar (nthcdr 2 kill) now))
-            ;; Old entry, remove.
-            ((and expire (< date expire))
-             (gnus-score-set 'touched '(t) (cdar words))
-             (setcdr (caar words) (cddaar words))))
+           (if (not trace)
+               (cond
+                ;; Permanent.
+                ((null date)
+                 )
+                ;; Match, update date.
+                ((and found gnus-update-score-entry-dates)
+                 (gnus-score-set 'touched '(t) (cdar words))
+                 (setcar (nthcdr 2 kill) now))
+                ;; Old entry, remove.
+                ((and expire (< date expire))
+                 (gnus-score-set 'touched '(t) (cdar words))
+                 (setcdr (caar words) (cddaar words)))))
            (setq words (cdr words))))))
     nil))
 
@@ -2056,6 +2113,10 @@ SCORE is the score to add."
       (set-syntax-table syntab))
     ;; Make all the ignorable words ignored.
     (let ((ignored (append gnus-ignored-adaptive-words
+                          (if gnus-adaptive-word-no-group-words
+                              (message-tokenize-header
+                               (gnus-group-real-name gnus-newsgroup-name)
+                               "."))
                           gnus-default-ignored-adaptive-words)))
       (while ignored
        (gnus-sethash (pop ignored) nil hashtb)))))
@@ -2184,6 +2245,11 @@ SCORE is the score to add."
            (set-syntax-table syntab))
          ;; Make all the ignorable words ignored.
          (let ((ignored (append gnus-ignored-adaptive-words
+                                (if gnus-adaptive-word-no-group-words
+                                    (message-tokenize-header
+                                     (gnus-group-real-name 
+                                      gnus-newsgroup-name)
+                                     "."))
                                 gnus-default-ignored-adaptive-words)))
            (while ignored
              (gnus-sethash (pop ignored) nil hashtb)))
@@ -2223,7 +2289,6 @@ SCORE is the score to add."
           1 "No score rules apply to the current article (default score %d)."
           gnus-summary-default-score)
        (set-buffer "*Score Trace*")
-       (gnus-add-current-to-buffer-list)
        (while trace
          (insert (format "%S  ->  %s\n" (cdar trace)
                          (if (caar trace)
@@ -2269,7 +2334,6 @@ SCORE is the score to add."
       (while rules
        (insert (format "%-5d: %s\n" (caar rules) (cdar rules)))
        (pop rules))
-      (gnus-add-current-to-buffer-list)
       (goto-char (point-min))
       (gnus-configure-windows 'score-words))))
 
@@ -2440,7 +2504,7 @@ GROUP using BNews sys file syntax."
         (trans (cdr (assq ?: nnheader-file-name-translation-alist)))
         ofiles not-match regexp)
     (save-excursion
-      (set-buffer (get-buffer-create "*gnus score files*"))
+      (set-buffer (gnus-get-buffer-create "*gnus score files*"))
       (buffer-disable-undo (current-buffer))
       ;; Go through all score file names and create regexp with them
       ;; as the source.
@@ -2673,8 +2737,7 @@ The list is determined from the variable gnus-score-file-alist."
       ((or (null newsgroup)
           (string-equal newsgroup ""))
        ;; The global score file is placed at top of the directory.
-       (expand-file-name
-       suffix gnus-kill-files-directory))
+       (expand-file-name suffix gnus-kill-files-directory))
       ((gnus-use-long-file-name 'not-score)
        ;; Append ".SCORE" to newsgroup name.
        (expand-file-name (concat (gnus-newsgroup-savable-name newsgroup)
@@ -2693,6 +2756,7 @@ The list is determined from the variable gnus-score-file-alist."
   (interactive (list gnus-global-score-files))
   (let (out)
     (while files
+      ;; #### /$ Unix-specific?
       (if (string-match "/$" (car files))
          (setq out (nconc (directory-files
                            (car files) t
@@ -2732,8 +2796,8 @@ If ADAPT, return the home adaptive file instead."
              (funcall elem group))
             ;; Regexp-file cons
             ((consp elem)
-             (when (string-match (car elem) group)
-               (cadr elem))))))
+             (when (string-match (gnus-globalify-regexp (car elem)) group)
+               (replace-match (cadr elem) t nil group ))))))
     (when found
       (nnheader-concat gnus-kill-files-directory found))))
 
@@ -2753,6 +2817,10 @@ If ADAPT, return the home adaptive file instead."
     (concat group (if (gnus-use-long-file-name 'not-score) "." "/")
            gnus-adaptive-file-suffix)))
 
+(defun gnus-current-home-score-file (group)
+  "Return the \"current\" regular score file."
+  (car (nreverse (gnus-score-find-alist group))))
+
 ;;;
 ;;; Score decays
 ;;;