Make commands use the inheritable rmeta-slot tooling
[mudsync.git] / mudsync / player.scm
1 ;;; Mudsync --- Live hackable MUD
2 ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
3 ;;;
4 ;;; This file is part of Mudsync.
5 ;;;
6 ;;; Mudsync is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or
9 ;;; (at your option) any later version.
10 ;;;
11 ;;; Mudsync is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 ;;; General Public License for more details.
15 ;;;
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with Mudsync.  If not, see <http://www.gnu.org/licenses/>.
18
19 (define-module (mudsync player)
20   #:use-module (mudsync command)
21   #:use-module (mudsync gameobj)
22   #:use-module (mudsync game-master)
23   #:use-module (mudsync parser)
24   #:use-module (8sync actors)
25   #:use-module (8sync agenda)
26   #:use-module (8sync rmeta-slot)
27   #:use-module (ice-9 control)
28   #:use-module (ice-9 format)
29   #:use-module (ice-9 match)
30   #:use-module (oop goops)
31   #:use-module (srfi srfi-1)
32   #:use-module (srfi srfi-9)
33   #:export (<player>))
34
35 ;;; Players
36 ;;; =======
37
38 (define-class <player> (<gameobj>)
39   (username #:init-keyword #:username
40             #:getter player-username)
41
42   (self-commands #:allocation #:each-subclass
43                  #:init-thunk
44                  (build-commands
45                   (("inventory" "inv" "i") ((empty-command cmd-inventory)))
46                   ("help" ((empty-command cmd-help)))))
47
48   (actions #:allocation #:each-subclass
49            #:init-thunk
50            (build-actions
51             (init player-init)
52             (handle-input player-handle-input)
53             (tell player-tell)
54             (disconnect-self-destruct player-disconnect-self-destruct)
55             (cmd-inventory player-cmd-inventory)
56             (cmd-help player-cmd-help))))
57
58
59 ;;; player message handlers
60
61 (define (player-init player message)
62   ;; Look around the room we're in
63   (<- (gameobj-loc player) 'look-room))
64
65
66 (define* (player-handle-input player message #:key input)
67   (define split-input (split-verb-and-rest input))
68   (define input-verb (car split-input))
69   (define input-rest (cdr split-input))
70
71   (define command-candidates
72     (player-gather-command-handlers player input-verb))
73
74   (define winner
75     (find-command-winner command-candidates input-rest))
76
77   (match winner
78     ((cmd-action winner-id message-args)
79      (apply <- winner-id cmd-action message-args))
80     (#f
81      (<- (gameobj-gm player) 'write-home
82          #:text "Sorry, I didn't understand that? (type \"help\" for common commands)\n"))))
83
84 (define* (player-tell player message #:key text)
85   (<- (gameobj-gm player) 'write-home
86       #:text text))
87
88 (define (player-disconnect-self-destruct player message)
89   "Action routine for being told to disconnect and self destruct."
90   (define loc (gameobj-loc player))
91   (when loc
92     (<- loc 'tell-room
93         #:exclude (actor-id player)
94         #:text (format #f "~a disappears in a puff of entropy!\n"
95                        (slot-ref player 'name))))
96   (gameobj-self-destruct player))
97
98 (define (player-cmd-inventory player message)
99   "Display the inventory for the player"
100   (define inv-names
101     (map
102      (lambda (inv-item)
103        (mbody-val (<-wait inv-item 'get-name)))
104      (gameobj-occupants player)))
105   (define text-to-show
106     (if (eq? inv-names '())
107         "You aren't carrying anything.\n"
108         (apply string-append
109                "You are carrying:\n"
110                (map (lambda (item-name)
111                       (string-append "  * " item-name "\n"))
112                     inv-names))))
113   (<- (actor-id player) 'tell #:text text-to-show))
114
115 (define (player-cmd-help player message)
116   (<- (actor-id player) 'tell
117       #:text '((strong "** Mudsync Help **")(br)
118                (p "You're playing Mudsync, a multiplayer text-adventure. "
119                   "Type different commands to interact with your surroundings "
120                   "and other players.")
121                (p "Some common commands:"
122                   (ul (li (strong "say <message>") " -- "
123                           "Chat with other players in the same room. "
124                           "(Also aliased to the " (b "\"") " character.)")
125                       (li (strong "look") " -- "
126                           "Look around the room you're in.")
127                       (li (strong "look [at] <object>") " -- "
128                           "Examine a particular object.")
129                       (li (strong "go <exit>") " -- "
130                           "Move to another room in <exit> direction.")))
131                (p "Different objects can be interacted with in different ways. "
132                   "For example, if there's a bell in the same room as you, "
133                   "you might try typing " (em "ring bell")
134                   " and see what happens."))))
135
136
137 ;;; Command handling
138 ;;; ================
139
140 ;; @@: Hard to know whether this should be in player.scm or here...
141 ;; @@: This could be more efficient as a stream...!?
142 (define (player-gather-command-handlers player verb)
143   (define player-loc
144     (let ((result (gameobj-loc player)))
145       (if result
146           result
147           (throw 'player-has-no-location
148                  "Player ~a has no location!  How'd that happen?\n"
149                  #:player-id (actor-id player)))))
150
151   ;; Ask the room for its commands
152   (define room-commands
153     ;; TODO: Map room id and sort
154     (mbody-receive (_ #:key commands)
155         (<-wait player-loc 'get-container-commands
156                 #:verb verb)
157       commands))
158
159   ;; All the co-occupants of the room (not including ourself)
160   (define co-occupants
161     (remove
162      (lambda (x) (equal? x (actor-id player)))
163      (mbody-receive (_ #:key occupants)
164          (<-wait player-loc 'get-occupants)
165        occupants)))
166
167   ;; @@: There's a race condition here if someone leaves the room
168   ;;   during this, heh...
169   ;;   I'm not sure it can be solved, but "lag" on the race can be
170   ;;   reduced maybe?
171
172   ;; Get all the co-occupants' commands
173   (define co-occupant-commands
174     (fold
175      (lambda (co-occupant prev)
176        (mbody-receive (_ #:key commands goes-by)
177            (<-wait co-occupant 'get-commands
178                    #:verb verb)
179          (append
180           (map (lambda (command)
181                  (list command goes-by co-occupant))
182                commands)
183           prev)))
184      '()
185      co-occupants))
186
187   ;; Append our own command handlers
188   (define our-commands
189     (class-rmeta-ref (class-of player) 'self-commands verb
190                      #:dflt '()))
191
192   ;; Append our inventory's relevant command handlers
193   (define inv-items
194     (gameobj-occupants player))
195   (define inv-item-commands
196     (fold
197      (lambda (inv-item prev)
198        (mbody-receive (_ #:key commands goes-by)
199            (<-wait inv-item 'get-contained-commands
200                    #:verb verb)
201          (append
202           (map (lambda (command)
203                  (list command goes-by inv-item))
204                commands)
205           prev)))
206      '()
207      inv-items))
208
209   ;; Now return a big ol sorted list of ((actor-id . command))
210   (append
211    (sort-commands-append-actor room-commands
212                                player-loc '()) ; room doesn't go by anything
213    (sort-commands-multi-actors co-occupant-commands)
214    (sort-commands-append-actor our-commands
215                                (actor-id player) '()) ; nor does player
216    (sort-commands-multi-actors inv-item-commands)))
217
218 (define (sort-commands-append-actor commands actor-id goes-by)
219   (sort-commands-multi-actors
220    (map (lambda (command) (list command goes-by actor-id)) commands)))
221
222 (define (sort-commands-multi-actors actors-and-commands)
223   (sort
224    actors-and-commands
225    (lambda (x y)
226      (> (command-priority (car x))
227         (command-priority (car y))))))
228
229
230 (define (find-command-winner sorted-candidates line)
231   "Find a command winner from a sorted list of candidates"
232   ;; A cache of results from matchers we've already seen
233   ;; TODO: fill in this cache.  This is a *critical* optimization!
234   (define matcher-cache '())
235   (call/ec
236    (lambda (return)
237      (for-each
238       (match-lambda
239         ((command actor-goes-by actor-id)
240          (let* ((matcher (command-matcher command))
241                 (matched (matcher line)))
242            (if (and matched
243                     ;; Great, it matched, but does it also pass
244                     ;; should-handle?
245                     (apply (command-should-handle command)
246                            actor-goes-by
247                            matched))  ; matched is kwargs if truthy
248                (return (list (command-action command)
249                              actor-id matched))
250                #f))))
251       sorted-candidates)
252      #f)))