6f2ddf071747d72118fb4c1b2057705955356dfe
[mudsync.git] / mudsync / gameobj.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 ;;; Game actor
20 ;;; ==========
21
22 (define-module (mudsync gameobj)
23   #:use-module (mudsync command)
24   #:use-module (8sync actors)
25   #:use-module (8sync agenda)
26   #:use-module (8sync rmeta-slot)
27   #:use-module (srfi srfi-1)
28   #:use-module (ice-9 control)
29   #:use-module (ice-9 format)
30   #:use-module (ice-9 match)
31   #:use-module (oop goops)
32   #:export (<gameobj>
33
34             gameobj-loc
35             gameobj-gm
36
37             gameobj-act-init
38             gameobj-set-loc!
39             gameobj-occupants
40             gameobj-self-destruct
41
42             slot-ref-maybe-runcheck
43             val-or-run
44
45             dyn-ref
46
47             ;; Some of the more common commands
48             cmd-take cmd-drop
49             cmd-take-from cmd-put-in))
50
51 ;;; Gameobj
52 ;;; =======
53
54
55 ;;; *all* game components that talk to players should somehow
56 ;;; derive from this class.
57 ;;; And all of them need a GM!
58
59 (define-class <gameobj> (<actor>)
60   ;; location id
61   (loc #:init-value #f
62        #:getter gameobj-loc)
63   
64   ;; Uses a hash table like a set (values ignored)
65   (occupants #:init-thunk make-hash-table)
66
67   ;; game master id
68   (gm #:init-keyword #:gm
69       #:getter gameobj-gm)
70
71   ;; a name to be known by
72   (name #:init-keyword #:name
73         #:init-value #f)
74   (goes-by #:init-keyword #:goes-by
75            #:init-value #f)
76
77   (desc #:init-value #f
78         #:init-keyword #:desc)
79
80   ;; Commands we can handle
81   (commands #:allocation #:each-subclass
82             #:init-thunk (build-commands
83                           ("take" ((direct-command cmd-take)
84                                    (prep-indir-command cmd-take-from
85                                                        '("from" "out of"))))
86                           ("put" ((prep-indir-command cmd-put-in
87                                                       '("in" "inside" "on"))))))
88
89   ;; Commands we can handle by being something's container
90   (container-commands #:allocation #:each-subclass
91                       #:init-thunk (build-commands))
92
93   ;; Commands we can handle by being contained by something else
94   (contained-commands #:allocation #:each-subclass
95                       #:init-thunk 
96                       (build-commands
97                        ("drop" ((direct-command cmd-drop #:obvious? #f)))))
98
99   ;; Most objects are generally visible by default
100   (invisible? #:init-value #f
101               #:init-keyword #:invisible?)
102   ;; TODO: Fold this into a procedure in invisible? similar
103   ;;   to take-me? and etc
104   (visible-to-player?
105    #:init-value (wrap-apply gameobj-visible-to-player?))
106
107   ;; Can be a boolean or a procedure accepting
108   ;; (gameobj whos-acting #:key from)
109   (take-me? #:init-value #f
110             #:init-keyword #:take-me?)
111   ;; Can be a boolean or a procedure accepting
112   ;; (gameobj whos-acting where)
113   (drop-me? #:init-value #t
114             #:init-keyword #:drop-me?)
115   ;; Can be a boolean or a procedure accepting
116   ;; (gameobj whos-acting take-what)
117   (take-from-me? #:init-value #f
118                  #:init-keyword #:take-from-me?)
119   ;; Can be a boolean or a procedure accepting
120   ;; (gameobj whos-acting put-what)
121   (put-in-me? #:init-value #f
122               #:init-keyword #:put-in-me?)
123
124   ;; TODO: Remove this and use actor-alive? instead.
125   ;; Set this on self-destruct
126   ;; (checked by some "long running" game routines)
127   (destructed #:init-value #f)
128
129   (actions #:allocation #:each-subclass
130            ;;; Actions supported by all gameobj
131            #:init-thunk
132            (build-actions
133             (init gameobj-act-init)
134             ;; Commands for co-occupants
135             (get-commands gameobj-get-commands)
136             ;; Commands for participants in a room
137             (get-container-commands gameobj-get-container-commands)
138             ;; Commands for inventory items, etc (occupants of the gameobj commanding)
139             (get-contained-commands gameobj-get-contained-commands)
140
141             (get-occupants gameobj-get-occupants)
142             (add-occupant! gameobj-add-occupant!)
143             (remove-occupant! gameobj-remove-occupant!)
144             (get-loc gameobj-act-get-loc)
145             (set-loc! gameobj-act-set-loc!)
146             (get-name gameobj-get-name)
147             (set-name! gameobj-act-set-name!)
148             (get-desc gameobj-get-desc)
149             (goes-by gameobj-act-goes-by)
150             (visible-name gameobj-visible-name)
151             (self-destruct gameobj-act-self-destruct)
152             (tell gameobj-tell-no-op)
153             (assist-replace gameobj-act-assist-replace)
154             (ok-to-drop-here? (lambda (gameobj message . _)
155                                 (<-reply message #t))) ; ok to drop by default
156             (ok-to-be-taken-from? gameobj-ok-to-be-taken-from)
157             (ok-to-be-put-in? gameobj-ok-to-be-put-in)
158
159             ;; Common commands
160             (cmd-take cmd-take)
161             (cmd-take-from cmd-take-from)
162             (cmd-put-in cmd-put-in)
163             (cmd-drop cmd-drop))))
164
165
166 ;;; gameobj message handlers
167 ;;; ========================
168
169 ;; Kind of a useful utility, maybe?
170 (define (simple-slot-getter slot)
171   (lambda (actor message)
172     (<-reply message (slot-ref actor slot))))
173
174 (define (gameobj-replace-step-occupants actor occupants)
175   ;; Snarf all the occupants!
176   (display "replacing occupant\n")
177   (when occupants
178     (for-each
179      (lambda (occupant)
180        (<-wait occupant 'set-loc!
181                #:loc (actor-id actor)))
182      occupants)))
183
184 (define gameobj-replace-steps*
185   (list gameobj-replace-step-occupants))
186
187 (define (run-replacement actor replaces replace-steps)
188   (when replaces
189     (mbody-receive (_ #:key occupants)
190         (<-wait replaces 'assist-replace)
191       (for-each
192        (lambda (replace-step)
193          (replace-step actor occupants))
194        replace-steps))))
195
196 ;; @@: This could be kind of a messy way of doing gameobj-act-init
197 ;;   stuff.  If only we had generic methods :(
198 (define* (gameobj-act-init actor message #:key replace)
199   "Your most basic game object init procedure.
200 Assists in its replacement of occupants if necessary and nothing else."
201   (run-replacement actor replace gameobj-replace-steps*))
202
203 (define (gameobj-goes-by gameobj)
204   "Find the name we go by.  Defaults to #:name if nothing else provided."
205   (cond ((slot-ref gameobj 'goes-by) =>
206          identity)
207         ((slot-ref gameobj 'name) =>
208          (lambda (name)
209            (list name)))
210         (else '())))
211
212 (define (gameobj-act-goes-by actor message)
213   "Reply to a message requesting what we go by."
214   (<-reply message #:goes-by (gameobj-goes-by actor)))
215
216 (define (val-or-run val-or-proc)
217   "Evaluate if a procedure, or just return otherwise"
218   (if (procedure? val-or-proc)
219       (val-or-proc)
220       val-or-proc))
221
222 (define (get-candidate-commands actor rmeta-sym verb)
223   (class-rmeta-ref (class-of actor) rmeta-sym verb
224                    #:dflt '()))
225
226 (define* (gameobj-get-commands actor message #:key verb)
227   "Get commands a co-occupant of the room might execute for VERB"
228   (define candidate-commands
229     (get-candidate-commands actor 'commands verb))
230   (<-reply message
231            #:commands candidate-commands
232            #:goes-by (gameobj-goes-by actor)))
233
234 (define* (gameobj-get-container-commands actor message #:key verb)
235   "Get commands as the container / room of message's sender"
236   (define candidate-commands
237     (get-candidate-commands actor 'container-commands verb))
238   (<-reply message #:commands candidate-commands))
239
240 (define* (gameobj-get-contained-commands actor message #:key verb)
241   "Get commands as being contained (eg inventory) of commanding gameobj"
242   (define candidate-commands
243     (get-candidate-commands actor 'contained-commands verb))
244   (<-reply message
245            #:commands candidate-commands
246            #:goes-by (gameobj-goes-by actor)))
247
248 (define* (gameobj-add-occupant! actor message #:key who)
249   "Add an actor to our list of present occupants"
250   (hash-set! (slot-ref actor 'occupants)
251              who #t))
252
253 (define* (gameobj-remove-occupant! actor message #:key who)
254   "Remove an occupant from the room."
255   (hash-remove! (slot-ref actor 'occupants) who))
256
257 (define* (gameobj-occupants gameobj #:key exclude)
258   (hash-fold
259    (lambda (occupant _ prev)
260      (define exclude-it?
261        (match exclude
262          ;; Empty list and #f are non-exclusion
263          (() #f)
264          (#f #f)
265          ;; A list of addresses... since our address object is (annoyingly)
266          ;; currently a simple cons cell...
267          ((exclude-1 ... exclude-rest)
268           (member occupant exclude))
269          ;; Must be an individual address!
270          (_ (equal? occupant exclude))))
271      (if exclude-it?
272          prev
273          (cons occupant prev)))
274    '()
275    (slot-ref gameobj 'occupants)))
276
277 (define* (gameobj-get-occupants actor message #:key exclude)
278   "Get all present occupants of the room."
279   (define occupants
280     (gameobj-occupants actor #:exclude exclude))
281   (<-reply message occupants))
282
283 (define (gameobj-act-get-loc actor message)
284   (<-reply message (slot-ref actor 'loc)))
285
286 (define (gameobj-set-loc! gameobj loc)
287   "Set the location of this object."
288   (define old-loc (gameobj-loc gameobj))
289   (format #t "DEBUG: Location set to ~s for ~s\n"
290           loc (actor-id-actor gameobj))
291
292   (when (not (equal? old-loc loc))
293     (slot-set! gameobj 'loc loc)
294     ;; Change registation of where we currently are
295     (if old-loc
296         (<-wait old-loc 'remove-occupant! #:who (actor-id gameobj)))
297     (if loc
298         (<-wait loc 'add-occupant! #:who (actor-id gameobj)))))
299
300 ;; @@: Should it really be #:id ?  Maybe #:loc-id or #:loc?
301 (define* (gameobj-act-set-loc! actor message #:key loc)
302   "Action routine to set the location."
303   (gameobj-set-loc! actor loc))
304
305 (define (slot-ref-maybe-runcheck gameobj slot whos-asking . other-args)
306   "Do a slot-ref on gameobj, evaluating it including ourselves
307 and whos-asking, and see if we should just return it or run it."
308   (match (slot-ref gameobj slot)
309     ((? procedure? slot-val-proc)
310      (apply slot-val-proc gameobj whos-asking other-args))
311     (anything-else anything-else)))
312
313 (define gameobj-get-name (simple-slot-getter 'name))
314
315 (define* (gameobj-act-set-name! actor message val)
316   (slot-set! actor 'name val))
317
318 (define* (gameobj-get-desc actor message #:key whos-looking)
319   (define desc-text
320     (match (slot-ref actor 'desc)
321       ((? procedure? desc-proc)
322        (desc-proc actor whos-looking))
323       (desc desc)))
324   (<-reply message desc-text))
325
326 (define (gameobj-visible-to-player? gameobj whos-looking)
327   "Check to see whether we're visible to the player or not.
328 By default, this is whether or not the generally-visible flag is set."
329   (not (slot-ref gameobj 'invisible?)))
330
331 (define* (gameobj-visible-name actor message #:key whos-looking)
332   ;; Are we visible?
333   (define we-are-visible
334     ((slot-ref actor 'visible-to-player?) actor whos-looking))
335
336   (define name-to-return
337     (if we-are-visible
338         ;; Return our name
339         (match (slot-ref actor 'name)
340           ((? procedure? name-proc)
341            (name-proc actor whos-looking))
342           ((? string? name)
343            name)
344           (#f #f))
345         #f))
346   (<-reply message #:text name-to-return))
347
348 (define (gameobj-self-destruct gameobj)
349   "General gameobj self destruction routine"
350   ;; Unregister from being in any particular room
351   (gameobj-set-loc! gameobj #f)
352   (slot-set! gameobj 'destructed #t)
353   ;; Boom!
354   (self-destruct gameobj))
355
356 (define* (gameobj-act-self-destruct gameobj message #:key why)
357   "Action routine for self destruction"
358   (gameobj-self-destruct gameobj))
359
360 ;; Unless an actor has a tell message, we just ignore it
361 (define gameobj-tell-no-op
362   (const 'no-op))
363
364 (define (gameobj-replace-data-occupants gameobj)
365   "The general purpose list of replacement data"
366   (list #:occupants (hash-map->list (lambda (occupant _) occupant)
367                                     (slot-ref gameobj 'occupants))))
368
369 (define (gameobj-replace-data* gameobj)
370   ;; For now, just call gameobj-replace-data-occupants.
371   ;; But there may be more in the future!
372   (gameobj-replace-data-occupants gameobj))
373
374 ;; So sad that objects must assist in their replacement ;_;
375 ;; But that's life in a live hacked game!
376 (define (gameobj-act-assist-replace gameobj message)
377   "Vanilla method for assisting in self-replacement for live hacking"
378   (apply <-reply message
379          (gameobj-replace-data* gameobj)))
380
381 (define (gameobj-ok-to-be-taken-from gameobj message whos-acting)
382   (<-reply message (slot-ref-maybe-runcheck gameobj 'take-me?
383                                             whos-acting #:from #t)))
384
385 (define (gameobj-ok-to-be-put-in gameobj message whos-acting where)
386   (<-reply message (slot-ref-maybe-runcheck gameobj 'drop-me?
387                                             whos-acting where)))
388
389 \f
390 ;;; Utilities every gameobj has
391 ;;; ---------------------------
392
393 (define (dyn-ref gameobj special-symbol)
394   "Dynamically look up a special object from the gm"
395   (match special-symbol
396     ;; if it's a symbol, look it up dynamically
397     ((? symbol? _)
398      (mbody-val (<-wait (slot-ref gameobj 'gm) 'lookup-special
399                         #:symbol special-symbol)))
400     ;; if it's false, return nothing
401     (#f #f)
402     ;; otherwise it's probably an address, return it as-is
403     (_ special-symbol)))
404
405
406 \f
407 ;;; Basic actions
408 ;;; -------------
409
410 (define* (cmd-take gameobj message
411                    #:key direct-obj
412                    (player (message-from message)))
413   (define player-name
414     (mbody-val (<-wait player 'get-name)))
415   (define player-loc
416     (mbody-val (<-wait player 'get-loc)))
417   (define our-name (slot-ref gameobj 'name))
418   (define self-should-take
419     (slot-ref-maybe-runcheck gameobj 'take-me? player))
420   ;; @@: Is there any reason to allow the room to object in the way
421   ;;   that there is for dropping?  It doesn't seem like it.
422   ;; TODO: Allow gameobj to customize
423   (if self-should-take
424       ;; Set the location to whoever's picking us up
425       (begin
426         (gameobj-set-loc! gameobj player)
427         (<- player 'tell
428             #:text (format #f "You pick up ~a.\n"
429                            our-name))
430         (<- player-loc 'tell-room
431             #:text (format #f "~a picks up ~a.\n"
432                            player-name
433                            our-name)
434             #:exclude player))
435       (<- player 'tell
436           #:text (format #f "It doesn't seem like you can take ~a.\n"
437                          our-name))))
438
439 (define* (cmd-drop gameobj message
440                    #:key direct-obj
441                    (player (message-from message)))
442   (define player-name
443     (mbody-val (<-wait player 'get-name)))
444   (define player-loc
445     (mbody-val (<-wait player 'get-loc)))
446   (define our-name (slot-ref gameobj 'name))
447   (define should-drop
448     (slot-ref-maybe-runcheck gameobj 'drop-me? player))
449   (define (room-objection-to-drop)
450     (mbody-receive (_ drop-ok? #:key why-not) ; does the room object to dropping?
451         (<-wait player-loc 'ok-to-drop-here? player (actor-id gameobj))
452       (and (not drop-ok?)
453            ;; Either give the specified reason, or give a boilerplate one
454            (or why-not
455                `("You'd love to drop " ,our-name
456                  " but for some reason it doesn't seem like you can"
457                  " do that here.")))))
458   (cond
459    ((not player-loc)
460     (<- player 'tell
461         #:text `("It doesn't seem like you can drop " ,our-name 
462                 " here, because you don't seem to be anywhere?!?")))
463    ;; TODO: Let ourselves supply a reason why not.
464    ((not should-drop)
465     (<- player 'tell
466         #:text (format #f "It doesn't seem like you can drop ~a.\n"
467                        our-name)))
468    ((room-objection-to-drop)
469     (<- player 'tell
470         #:text room-objection-to-drop))
471    (else
472     (gameobj-set-loc! gameobj player-loc)
473     ;; TODO: Allow more flavortext here.
474     (<- player 'tell
475         #:text (format #f "You drop ~a.\n"
476                        our-name))
477     (<- player-loc 'tell-room
478         #:text (format #f "~a drops ~a.\n"
479                        player-name
480                        our-name)
481         #:exclude player))))
482
483 ;; @@: Moving this to a container subclass/mixin could allow a lot more
484 ;;   customization of take out / put in phrases
485 (define* (cmd-take-from gameobj message
486                         #:key direct-obj indir-obj preposition
487                         (player (message-from message)))
488   (define player-name
489     (mbody-val (<-wait player 'get-name)))
490   (define player-loc
491     (mbody-val (<-wait player 'get-loc)))
492   (define our-name (slot-ref gameobj 'name))
493   ;; We need to check if we even have such a thing
494   (define this-thing
495     (call/ec
496      (lambda (return)
497        (for-each (lambda (occupant)
498                    (mbody-receive (_ #:key goes-by)
499                        (<-wait occupant 'goes-by)
500                      (when (member direct-obj goes-by)
501                        (return occupant))))
502                  (gameobj-occupants gameobj))
503        ;; nothing found
504        #f)))
505   (define (this-thing-name)
506     (mbody-val (<-wait this-thing 'get-name)))
507   (define (should-take-from-me)
508     (and this-thing
509          (slot-ref-maybe-runcheck gameobj 'take-from-me? player this-thing)))
510   (define (default-objection)
511     `("Unfortunately, it doesn't seem like you can take "
512       (this-thing-name) " " preposition " " our-name "."))
513
514   (define (this-thing-objection)
515     (mbody-receive (_ taken-ok? #:key why-not) ; does the object object to being removed?
516         (<-wait this-thing 'ok-to-be-taken-from? player) ; @@ no need to supply from where
517       (and (not taken-ok?)
518            ;; Either give the specified reason, or give a boilerplate one
519            (or why-not
520                (default-objection)))))
521   (cond
522    ;; Wait, aren't we going to check (should-take-from-me) later?
523    ;; Well yes, but this checks if there's a #f as the value, which
524    ;; is a much clearer indication that this doesn't take *anything*.
525    ((not (slot-ref gameobj 'take-from-me?))
526     (<- player 'tell
527         #:text `("It's not really clear how to take something " ,preposition
528                  " " ,our-name ".")))
529
530    ;; Unfortunately this does leak information about what is contained
531    ;; by us.  Maybe not what's wanted in all circumstances.
532    ((not this-thing)
533     (<- player 'tell
534         #:text `("You don't see any such " ,direct-obj " to take from "
535                  ,our-name ".")))
536    ;; A particular objection to taking this thing.
537    ;; We should allow customizing the reason here, which could be
538    ;; provided by the 'ok-to-be-taken-from? slot.
539    ((not (should-take-from-me))
540     (<- player 'tell
541         #:text (default-objection)))
542    ;; the thing we wsant to take itself has objected...
543    ((this-thing-objection) =>
544     (lambda (objection)
545       (<- player 'tell
546           #:text objection)))
547    ;; looks like we can take it
548    (else
549     ;; Wait to announce to the player just in case settting the location
550     ;; errors out or something.  Maybe it's overthinking things, I dunno.
551     (<-wait this-thing 'set-loc! #:loc player)
552     (<- player 'tell
553         #:text `("You take " ,(this-thing-name) " from "
554                  ,our-name "."))
555     (<- player-loc 'tell-room
556         #:text `(,player-name " takes " ,(this-thing-name) " from "
557                               ,our-name ".")
558         #:exclude player))))
559
560 (define* (cmd-put-in gameobj message
561                      #:key direct-obj indir-obj preposition
562                      (player (message-from message)))
563   (define player-name
564     (mbody-val (<-wait player 'get-name)))
565   (define player-loc
566     (mbody-val (<-wait player 'get-loc)))
567   (define our-name (slot-ref gameobj 'name))
568   ;; We need to check if we even have such a thing
569   (define this-thing
570     (call/ec
571      (lambda (return)
572        (for-each (lambda (occupant)
573                    (mbody-receive (_ #:key goes-by)
574                        (<-wait occupant 'goes-by)
575                      (when (member direct-obj goes-by)
576                        (return occupant))))
577                  (mbody-val (<-wait player 'get-occupants)))
578        ;; nothing found
579        #f)))
580   (define (this-thing-name)
581     (mbody-val (<-wait this-thing 'get-name)))
582   (define (should-put-in-me)
583     (and this-thing
584          (slot-ref-maybe-runcheck gameobj 'put-in-me? player this-thing)))
585   (define (default-objection)
586     `("As much as you'd like to, it doesn't seem like you can put "
587       ,(this-thing-name) " " ,preposition " " ,our-name "."))
588   (define (this-thing-objection)
589     (mbody-receive (_ put-in-ok? #:key why-not) ; does the object object to being moved?
590         (<-wait this-thing 'ok-to-be-put-in? player (actor-id gameobj))
591       (and (not put-in-ok?)
592            ;; Either give the specified reason, or give a boilerplate one
593            (or why-not (default-objection)))))
594   (cond
595    ;; Is it not there, or maybe we won't allow it to be taken?
596    ((not this-thing)
597     (<- player 'tell
598         #:text `("You don't seem to have any such " ,direct-obj " to put "
599                  ,preposition " " ,our-name ".")))
600
601    ((or (not (should-put-in-me)))
602     (<- player 'tell
603         #:text (default-objection)))
604    ;; the thing we wsant to take itself has objected...
605    ((this-thing-objection) =>
606     (lambda (objection)
607       (<- player 'tell
608           #:text objection)))
609    ;; looks like we can take it
610    (else
611     ;; Wait to announce to the player just in case settting the location
612     ;; errors out or something.  Maybe it's overthinking things, I dunno.
613     (<-wait this-thing 'set-loc! #:loc (actor-id gameobj))
614     (<- player 'tell
615         #:text `("You put " ,(this-thing-name) " " ,preposition " "
616                  ,our-name "."))
617     (<- player-loc 'tell-room
618         #:text `(,player-name " puts " ,(this-thing-name) " " ,preposition " "
619                               ,our-name ".")
620         #:exclude player))))