root/trunk/lib/fm/composedobject.rb

Revision 288, 15.0 kB (checked in by ged, 3 months ago)
  • Updated build system
  • Finished converting statistic tests to specs.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Rev Author URL Id
Line 
1#!/usr/bin/ruby
2#
3# This file contains the FaerieMUD::ComposedObject class, the
4# FaerieMUD::ComposedObject::Constituent mixin, and the
5# FaerieMUD::ComposedObject::Bifurcation class.
6#
7# FaerieMUD::ComposedObject is a derivative of FaerieMUD::Entity which is an
8# attempt to model game objects which possess a high degree of individuality and
9# internal complexity in a balanceable, predictable, and reusable way. This is
10# done by dividing the functionality of the object into a number of subordinate
11# parts, each with a statistic, and then giving the primary container object a
12# balanced number of traits that derive from one or more of the statistics.
13#
14# FaerieMUD::ComposedObject::Constituent is a mixin which adds the necessary data
15# structures to an object class to make it capable of being used as a constituent
16# in a ComposedObject instance.
17#
18# == Synopsis
19#
20#   class ComposedThingie < FaerieMUD::ComposedObject
21#       class Alpha < FaerieMUD::Entity
22#           include FaerieMUD::ComposedObject::Constituent
23#           def_statistic :foo, :bar
24#       end
25#       class Beta < FaerieMUD::Entity
26#           include FaerieMUD::ComposedObject::Constituent
27#           def_statistic :jim, :bob
28#       end
29#       class Gamma < FaerieMUD::Entity
30#           include FaerieMUD::ComposedObject::Constituent
31#           def_statistic :foo, :bar
32#       end
33#   
34#       def_constituents :alpha => Alpha, :beta => Beta, :gamma => Gamma
35#   end
36#
37# == Subversion ID
38#
39# $Id$
40#
41# == Authors
42#
43# * Michael Granger <ged@FaerieMUD.org>
44#
45# :include: LICENSE
46#
47#---
48#
49# Please see the file LICENSE for licensing details.
50#
51
52require 'fm/mixins'
53require 'fm/exceptions'
54require 'fm/statistic'
55require 'fm/entity'
56
57### The (abstract) ComposedObject class. Instances of derivatives of this
58### class are aggregate objects which contain a balanced set of subordinate
59### parts which together define the behaviour of the whole.
60class FaerieMUD::ComposedObject < FaerieMUD::Entity
61        include FaerieMUD::AbstractClass
62       
63        contributors :ged, :scotus
64
65        ### Mixin that designates an object class as a constituent of a
66        ### ComposedObject. Adds a 'statistic' attribute and accessors.
67        module Constituent
68
69                ### Methods which are added to including classes as class methods.
70                module ClassMethods
71                        # A two-element Array containing the names of the aspects of
72                        # the statistic that belongs to the Constituent --
73                        # developmental and linear.
74                        attr_reader :statistic_names
75
76                        ### Create accessors for the constituent's
77                        ### FaerieMUD::Statistic object with the given +devel_name+,
78                        ### which will return the Statistic's developmental aspect,
79                        ### and +linear_name+, which will return the Statistic's
80                        ### linear constituent.
81                        def def_statistic( devel_name, linear_name )
82                                devel = devel_name.to_s.intern
83                                linear = linear_name.to_s.intern
84
85                                self.statistic_names.replace([ devel_name, linear_name ])
86
87                                define_method( devel ) { self.statistic.developmental }
88                                define_method( "#{devel}=" ) {|arg|
89                                        self.statistic.developmental = arg
90                                }
91
92                                define_method( linear ) { self.statistic.linear }
93                                define_method( "#{linear}=" ) {|arg|
94                                        self.statistic.linear = arg
95                                }
96                        end
97                end
98
99
100                ### Inclusion callback -- Install a 'def_statistic' function in
101                ### including classes to auto-generate statistic accessors.
102                def self::included( klass )
103                        klass.instance_variable_set( :@statistic_names, [] )
104                        klass.extend( ClassMethods )
105                end
106
107
108                #############################################################
109                ###     I N S T A N C E   M E T H O D S
110                #############################################################
111
112                ### Initialize the "statistic" part of a Constituent object. It uses
113                ### the ':statistic' member of the argument hash, if one exists.
114                def initialize( args={} )
115                        raise RuntimeError, "No statistic defined for #{self.class.inspect}" if
116                                self.class.statistic_names.empty?
117
118                        # If the statistic is defined in the args, remove it from the
119                        # arglist and use it as the element. If it's not in the arglist,
120                        # just use Hydrogen.
121                        element = args.delete( :statistic ) || 1
122                        devel, linear = self.class.statistic_names
123                        @statistic = FaerieMUD::Statistic.new( element, devel, linear )
124
125                        super
126                end
127
128
129                ### Copy constructor -- clone the +original+ object's statistic.
130                def initialize_copy( original )
131                        super
132                        @statistic = original.statistic.clone
133                end
134
135
136                ######
137                public
138                ######
139
140                # The FaerieMUD::Statistic associated with the receiver.
141                attr_accessor :statistic
142
143                ### Return a String containing a human-readable version of the
144                ### statistic.
145                def to_s
146                        "<%s: %s>" % [ self.class.name, self.statistic.to_s ]
147                end
148
149        end # module Constituent
150
151
152        ### Abstract base class for bifurcations of one of a ComposedObject's
153        ### constituents. Bifurcations provide a container for constituents and
154        ### a "picking" function which returns the constituent which is
155        ### "current" according to some encapsulated criteria. Concrete
156        ### derivatives are required to implement the #current method, which
157        ### should return one of the constituents it contains. Bifurcation
158        ### objects will autoload delegator methods for any method that its
159        ### constituents respond to as they are called; creating a Bifurcation
160        ### with Constituents of unrelated classes may cause this mechanism to
161        ### break.
162        class Bifurcation < FaerieMUD::Entity
163                include FaerieMUD::AbstractClass
164               
165                contributors :ged
166
167                ### Initialize a new Bifucation that will delegate the containing
168                ### ComposedObject's interaction with the given +constituents+.
169                def initialize( *constituents ) # :notnew:
170                        raise ArgumentError, "Bifurcation requires two or more Constituents" unless
171                                constituents.length >= 2
172                        check_each_type( constituents, FaerieMUD::Entity )
173                        @constituents = constituents
174                end
175
176
177                ### Copy initializer -- initialize a cloned or duped Bifurcation
178                ### with cloned values from the +original+.
179                def initialize_copy( original )
180                        super
181                        @constituents = original.constituents.collect {|constituent|
182                                constituent.clone
183                        }
184                end
185
186
187                ######
188                public
189                ######
190
191                # The constituents which make up the bifurcation
192                attr_accessor :constituents
193
194
195                ### Picker method: should return the "current" constituent.
196                virtual :current
197
198
199                #########
200                protected
201                #########
202
203                ### Autoloading method delegation -- create delegator methods for
204                ### anything which the underlying objects support.
205                def method_missing( sym, *args )
206                        constituent = self.current
207                        if constituent.respond_to?( sym )
208                                eval %{def #{sym}( *args ) self.current.#{sym}(*args) end}
209                                constituent.send( sym, *args )
210                        else
211                                super
212                        end
213                end
214        end # class Bifurcation
215
216
217
218        #############################################################
219        ###     C O N S T A N T S
220        #############################################################
221
222        # SVN Revision
223        SVNRev = %q$Rev$
224
225        # SVN Id
226        SVNId = %q$Id$
227
228        # The factor which is applied to traits' modifiers in the calculation of
229        # Effective Trait Levels.
230        TraitWeightingFactor = 1.0
231
232
233        #############################################################
234        ###     C L A S S   M E T H O D S
235        #############################################################
236
237        ### Inheritance callback -- adds two class instance variables to each
238        ### inheriting class to keep track of its constituents and traits.
239        def self::inherited( klass )
240                klass.add_structure_variables
241                super
242        end
243
244
245        ### Add structure-defining instance variables to subclasses
246        def self::add_structure_variables
247                # Don't clobber variables that are already there.
248                unless self.instance_variables.include?( "@constituent_map" ) &&
249                                self.instance_variables.include?( "@trait_map" )
250
251                        # Create class instance variables
252                        self.instance_variable_set( :@constituent_map, {} )
253                        self.instance_variable_set( :@trait_map, {} )
254
255                        class << self
256                                attr_accessor :constituent_map
257                                attr_accessor :trait_map
258                        end
259                end
260        end
261
262
263        ### Define constituent accessors for the calling class. Each entry in the
264        ### +constituent_hash+ should be of the form:
265        ###
266        ###     :name   => <em>ConstituentClass<em>
267        ###
268        ### ComposedObject derivatives must have three or more constituents, and
269        ### there must be an odd number of them.
270        def self::def_constituents( constituent_hash )
271                self.add_structure_variables
272
273                # Check the hash for sanity
274                if constituent_hash.length < 3
275                        raise FaerieMUD::ConstituentError,
276                                "Insufficient number of constituents."
277                elsif (constituent_hash.length % 2).zero?
278                        raise FaerieMUD::ConstituentError,
279                                "There must be an odd number of constituents."
280                end
281
282                create_constituent_methods( constituent_hash )
283                self.constituent_map.replace( constituent_hash )
284        end
285
286
287        ### Check the given hash of constituent classes (like that passed to
288        ### def_constituents) for collision.
289        def self::check_constituent_hash( constituent_hash )
290
291                # Keep track of statistic aspect names we've already seen.
292                statAspects = {}
293               
294                # Check the hash for valid members and collisions between the names
295                # of the aspects of the constituents' statistics.
296                constituent_hash.each {|name, klass|
297                        unless klass < FaerieMUD::ComposedObject::Constituent
298                                raise FaerieMUD::ConstituentError,
299                                        "Invalid constituent '#{klass.name}': Does not "\
300                                        "include ComposedObject::Constituent"
301                        end
302
303                        # Make sure none of the aspect-names for the constituents'
304                        # statistics collide with each other.
305                        develAspect, linearAspect = klass.statistic_names
306                        if statAspects.key?( develAspect )
307                                raise FaerieMUD::ConstituentError,
308                                        "Developmental aspect '%s' of %s "\
309                                        "collides with the %s aspect of %s" % [
310                                        develAspect,
311                                        klass,
312                                        statAspects[develAspect][:type],
313                                        statAspects[develAspect][:class],
314                                ]
315                        elsif statAspects.key?( linearAspect )
316                                raise FaerieMUD::ConstituentError,
317                                        "Linear aspect '%s' of %s "\
318                                        "collides with the %s aspect of %s" % [
319                                        linearAspect,
320                                        klass,
321                                        statAspects[linearAspect][:type],
322                                        statAspects[linearAspect][:class],
323                                ]
324                        end
325                        statAspects[develAspect] =
326                                { :type => 'developmental', :class => klass }
327                        statAspects[linearAspect] =
328                                { :type => 'linear', :class => klass }
329                }
330
331                return true
332        end
333
334
335        ### Create accessors for both the constituents themselves, and delegated
336        ### ones for the 2 aspects of their statistics.
337        def self::create_constituent_methods( constituent_hash )
338                check_constituent_hash( constituent_hash )
339
340                constituent_hash.each {|name, klass|
341
342                        # Gather symbols for the methods we'll be creating
343                        nameEq = "#{name}="
344                        develAspect, linearAspect = klass.statistic_names
345                        develAspectEq = "#{develAspect}=".intern
346                        linearAspectEq = "#{linearAspect}=".intern
347
348                        # Create the constituent accessors
349                        define_method( name ) { @constituents[name] }
350                        define_method( nameEq ) {|arg|
351                                check_type( arg, self.class.constituent_map[name] )
352                                @constituents[ name ] = arg
353                        }
354
355                        # Create the delegated accessors for the statistics of the two
356                        # aspects of the constituent.
357                        define_method( develAspect ) {
358                                self.send(name).statistic.developmental
359                        }
360                        define_method( develAspectEq ) {|arg|
361                                self.send(name).statistic.developmental = arg
362                        }
363                        define_method( linearAspect ) {
364                                self.send(name).statistic.linear
365                        }
366                        define_method( linearAspectEq ) {|arg|
367                                self.send(name).statistic.linear = arg
368                        }
369                }
370        end
371
372
373        ### Define a trait with the specified +name+ (a Symbol) in the calling
374        ### class. The trait will be derived from the statistics of the named
375        ### +constituents+ (also Symbols), which must be given in the order in
376        ### which they are used, with the primary constituent first.
377        def self::def_trait( name, *constituents )
378                self.add_structure_variables
379
380                raise FaerieMUD::ConstituentError,
381                        "Trait must be derived from at least one constituent." if
382                        constituents.empty?
383
384                self.trait_map[ name ] = constituents
385                code = self.build_trait_methods( name, *constituents )
386
387                module_eval( code )
388        end
389
390
391        ### Generate the source for trait methods to derive the value for the
392        ### specified +trait+ (a Symbol) that uses the statistics of the given
393        ### +constituents+ (Symbols).
394        def self::build_trait_methods( trait, *constituents )
395                # Generate statistic-fetching code for each relevant constituent
396                fetchStats = ''
397                constituents.each_with_index do |constituent, i|
398                        fetchStats += 
399                                "linear[#{i}] = @constituents[ :#{constituent} ].statistic."\
400                                "linear.to_f\n"\
401                                "devel[#{i}] = @constituents[ :#{constituent} ].statistic."\
402                                "developmental.to_f\n"
403                end
404
405
406                # Set the algorithm based on the number of constituents. 'stat' is the
407                # array of values derived from the statistic.
408                case constituents.length
409
410                # ETL = (T + S1)/2
411                when 1
412                        algorithm = "(( tmod * twf ) + stat[0]) / 2"
413
414                # ETL = (T/2) + (2S1/6) + (S2/6)
415                when 2
416                        algorithm = "(tmod * twf)/2 + (2*stat[0])/6 + (stat[1]/6)"
417
418                # ETL = (T/2) + (S1/4) + (S2/6) + (S3/12)
419                when 3
420                        algorithm = "(tmod * twf)/2 + (stat[0]/4) + (stat[1]/6) + (stat[2]/12)"
421
422                # :TODO: Fill in the rest -- should really be algorithmic
423                # code-generation, but I can't see how to do it yet.
424                else
425                        raise ConstituentError,
426                                "Unhandled composite: Can't yet handle #{constituents.length} "\
427                                "statistical inputs."
428                end
429
430                # For the trait value, the statistic's value is (linear+devel)/2;
431                # for the trait_max, it's just the developmental part.
432                return %Q{
433                        def #{trait}
434                                linear = []; devel = []
435                                #{fetchStats}
436                                stat = []
437                                linear.nitems.times {|i|
438                                        stat[i] = (linear[i] + devel[i]) / 2
439                                }
440                                twf = TraitWeightingFactor
441                                tmod = @traitModifiers[:#{trait}].to_f
442
443                                return Integer( #{algorithm} )
444                        end
445                        def #{trait}_max
446                                linear = []; devel = []
447                                #{fetchStats}
448                                stat = devel
449                                twf = TraitWeightingFactor
450                                tmod = @traitModifiers[:#{trait}].to_f
451
452                                return Integer( #{algorithm} )
453                        end
454                }
455        end
456
457
458        #############################################################
459        ###     I N S T A N C E   M E T H O D S
460        #############################################################
461
462        ### Create a new ComposedObject.
463        def initialize( args={} )
464                raise RuntimeError, "No constituents defined for #{self.class.name}" if
465                        self.class.constituent_map.empty?
466
467                # Set up default constituents
468                @constituents = {}
469                self.class.constituent_map.each {|key,klass|
470                        @constituents[key] = klass.new
471                }
472
473                # Set up default trait modifiers
474                @traitModifiers = {}
475                self.class.trait_map.keys.each {|trait|
476                        @traitModifiers[trait] = 
477                                FaerieMUD::PeriodicObject.new( :lithium )
478                }
479
480                super
481        end
482
483
484        ### Copy initializer -- initialize a cloned or duped ComposedObject with
485        ### cloned values from the +original+ object.
486        def initialize_copy( original )
487                super
488
489                @constituents = {}
490                original.constituents.each {|k,v|
491                        @constituents[k] = v.clone
492                }
493                @traitModifiers = original.traitModifiers.clone
494        end
495
496
497        ######
498        public
499        ######
500
501        # The Hash of constituents that the receiver delegates to, keyed by
502        # Symbol.
503        attr_accessor :constituents
504
505        # The Hash of trait modifiers for this composed object, keyed by trait
506        # name.
507        attr_accessor :traitModifiers
508
509
510        ### Return a snapshot of the current values of the ComposedObject's traits,
511        ### keyed by name.
512        def traits
513                rval = {}
514
515                self.class.trait_map.keys.each do |trait|
516                        rval[ trait ] = self.send( trait )
517                end
518
519                return rval
520        end
521
522
523end # class FaerieMUD::ComposedObject
Note: See TracBrowser for help on using the browser.