If I have a Rails model for something like Quests (like in a game) I'd have attributes like name, description, ect. but I'd also need some code associated with each quest to determine if the requirements have been met by a player to complete the quest.
What is the best way to associate some code with records from a model? Should I make a quest_function attribute in the model and write a function for each record in the models/quest.rb file when I create a new quest? This seems clunky and error prone.
How should something like this be done in Rails?
Well, you have several options.
The first, like you said, is to store a method/function name in your record and then call that method whenever you need to evaluate whether that Quest for that record has been completed. In rough code:
class Quest < ActiveRecord::Base
attr_accessible :name, :quest_method
def completed?
self.send(quest_method)
end
def end_quest
Monster.find_by_name('FINAL BOSS').dead?
end
end
Quest.create name: 'End Quest', quest_method: 'end_quest'
This isn't quite as bad as you think. This is essentially how single table inheritance works, but instead of storing a method name it stores the class name in the type
column.
class Quest < ActiveRecord::Base
def finished?
# default value, or throw an error
false
end
end
class EndQuest < Quest
def finished?
Monster.find_by_name('END BOSS').dead?
end
end
EndQuest.create # have to do this for all quests
In either case, you're writing the behavior as code, and just storing a reference to the desired behavior in your record. You do have to create all the records by hand with the proper types or method names and make sure everything stays in sync. But at least with single-table inheritance ActiveRecord takes care of some of that.
There's nothing stopping you, however, from storing code itself into the database.
class Quest < ActiveRecord::Base
attr_accessible :name, :completed_expr
def completed?
eval(completed_expr)
end
end
Quest.create name: 'END QUEST',
completed_expr: "Monster.find_by_name('BIG BOSS').dead?"
This, of course is potentially dangerous and error prone unless you know exactly what you're doing—but it does give you complete flexibility to rewrite the quest completion criteria at run-time.
Finally, if you want the most sophistication and complexity, you might want to look at using a rules engine (or, rolling out your own complete with DSL) to encompass your business (game world) rules and store that.
class Quest < ActiveRecord::Base
attr_accessible :name, :completion_rule
def completed?
RulesEngine.evaluate(completion_rule)
end
end
Quest.create name: 'END QUEST',
completion_rule: "Monster(name: 'BIG BOSS', state: 'dead')"
The main advantage is by storing your quest-specific behavior or rule in a DSL (which is not raw Ruby code) you get some level of security—you can't just accidentally eval
malicious or errant code and bring the house down.
You can also gain some efficiency if your production rules are modeled in such a way where you can use something like the Rete algorithm to evaluate them.