The Artima Developer Community
Sponsored Link

Ruby Buzz Forum
Classes on the Fly

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Matt Williams

Posts: 466
Nickname: aetherical
Registered: Feb, 2008

Matt Williams is a jack-of-all trades living in Columbus, OH.
Classes on the Fly Posted: Apr 25, 2008 11:41 AM
Reply to this message Reply

This post originated from an RSS feed registered with Ruby Buzz by Matt Williams.
Original Post: Classes on the Fly
Feed Title: Ruby Blender
Feed URL: http://feeds2.feedburner.com/RubyBlender
Feed Description: This blog contains short-ish ruby tips, hints, and techniques.
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by Matt Williams
Latest Posts From Ruby Blender

Advertisement
I recently had a case (a game) where I had the potential for many classes which were similar, but have enough differences it made sense to dry them up, so I developed a method to use yaml to define a class.

In my development of Machines, Monsters, and Madness, I've taken what might be described as a side excursion: I'm documenting the process of creating a simple wargame in Ruby which encompasses a library of game related utilities which will make its way into M3.

As it turns out, there are many unit and terrain types. These have a lot of overlap as well as there being many, many classes which would be needed. So, I've come up with a way to dry up my code and use a yaml file for generating these classes. With very little expansion, it could be used for a more generic solution (I'll get into that below).

I start with _why's Creature class from DWEMTHY's array ($$ DWEMTHY_S ARRAY ^!^ A RUBY MINI_DUNGEON ^!^ ONLY 60 LINES OF ...), but using the updated version from Seeing Metaclasses Clearly, which uses a helper called metaid.rb to encapsulate much of the meta-classiness:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

 class Object
   # The hidden singleton lurks behind everyone
   def metaclass; class << self; self; end; end
   def meta_eval &blk; metaclass.instance_eval &blk; end

   # Adds methods to a metaclass
   def meta_def name, &blk
     meta_eval { define_method name, &blk }
   end

   # Defines an instance method within a class
   def class_def name, &blk
     class_eval { define_method name, &blk }
   end
 end

This leads to a modified version of creature.rb which I am calling traiter.rb (because it defines classes with traits, don'tcha know?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

 class Object
   # The hidden singleton lurks behind everyone
   def metaclass; class << self; self; end; end
   def meta_eval &blk; metaclass.instance_eval &blk; end

   # Adds methods to a metaclass
   def meta_def name, &blk
     meta_eval { define_method name, &blk }
   end

   # Defines an instance method within a class
   def class_def name, &blk
     class_eval { define_method name, &blk }
   end
 end

I've split this out from <strike>Creature</strike>Unit, because I will later use the same method to define terrain. Then the Unit follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

require 'traiter'

class Unit < Traiter

  traits :attack, :defence, :movement, :zone_of_control, :vision, :strength,
    :location, :cost, :range, :x, :y, :symbol, :size


  def see?(x,y)
    distance = Map.distance(@location, [x,y])
    return (distance <= @vision)
  end

  def move(x,y)
    distance = Map.distance(@location, [x,y])
    @location = [x, y] if (distance <= @location)
    return @location
  end

end

From here, the yaml code to define my units has two parts:

  1. Templates for the various unit types; to keep it dry.
  2. Class definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

---
templates:
  soldier:
    movement: 1
    vision: 1
    zone_of_control: 1
    range: 1
  scout:
    movement: 3
    vision: 2
    zone_of_control: 1
    range: 1
classes:
- name: militia
  template: soldier
  attributes:
    strength: 50
    attack: -10
    defence: 10
    cost: 50
    symbol: m
- name: rifleman
  template: soldier
  attributes:
    strength: 100
    attack: 30
    defence: 30
    cost: 125
    symbol: r
- name: scout
  template: scout
  attributes:
    strength: 20
    attack: 10
    defence: 10
    cost: 200
    symbol: s

In each of the classes, I am declaring an (optional) template, as well as (optional) attributes. The code which performs the loading of the yaml and the creation of the classes follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

require 'yaml'

class Loader
  def self.load_classes(file, parent = nil)
    begin
      y = YAML.load(File.open(file))
      templates = y["templates"]
      klasses = y["classes"]
      klasses.each do |klass|
        puts klass['name']
        k = Object.const_set(klass['name'].capitalize,
                             Class.new(parent || Object.const_get(klass['parent'].capitalize)))
        k.class_eval  <<EOF
  #{(klass['traits'].nil? ? "" : "traits #{klass['traits'].map{ |trait| ":#{trait}"}.join(",")}")}
  #{(templates["#{klass['template']}"] || {}).merge((klass['attributes'] || {})).sort.map{|t| t[1]="'#{t[1]}'" if t[1].instance_of?(String); t.join(" ").downcase}.join("\n")}
EOF
      end
    rescue Exception => e
      puts "Error parsing File #{file}: #{e}"
    end
  end
end

The method is a class method, so it's invoked as Loader.load_classes(file, Unit); if I choose not to pass in the second argument, then I need to specify classes in my yaml file for each class. I then load the yaml and for clarity split it into templates and classes. Then for each class, I do the following:

  1. Output the class name; this could be suppressed if desired
  2. Create a Class instance either using the parent argument or doing a lookup to find the parent and register it with Object.
  3. Taking the class instance, I then add to it with class_eval and add the following:
    1. Any additional traits which might be defined for the class
    2. Merging the (optional) template's traits and the class' (optional) traits, I tranform them as follows:
      • Numerics are changed to "trait amount", as in "strength 10"
      • Strings are escaped to look like "symbol 's'"

There's definite places for expansion/improvement -- I could modify it so that a trait could be an array or a hash. also I could add the ability to embed code in the yaml file. But, all in all, it does save typing and makes my definition of units and terrains more flexible.

I'll gladly listen to any suggestions for improvement

Read: Classes on the Fly

Topic: Agile Web Development with Rails, Third Edition Previous Topic   Next Topic Topic: It's About Time!

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use