6

我正在尝试在 Ruby on Rails 中设计一个成就系统,但我的设计/代码遇到了障碍。

尝试使用多态关联:

class Achievement < ActiveRecord::Base
  belongs_to :achievable, :polymorphic => true
end

class WeightAchievement < ActiveRecord::Base
  has_one :achievement, :as => :achievable
end

迁移:

class CreateAchievements < ActiveRecord::Migration
... #code
    create_table :achievements do |t|
      t.string :name
      t.text :description
      t.references :achievable, :polymorphic => true

      t.timestamps
    end

     create_table :weight_achievements do |t|
      t.integer :weight_required
      t.references :exercises, :null => false

      t.timestamps
    end
 ... #code
end

然后,当我尝试以下一次性单元测试时,它失败了,因为它说成就为空。

test "parent achievement exists" do
   weightAchievement = WeightAchievement.find(1)
   achievement = weightAchievement.achievement 

    assert_not_nil achievement
    assert_equal 500, weightAchievement.weight_required
    assert_equal achievement.name, "Brick House Baby!"
    assert_equal achievement.description, "Squat 500 lbs"
  end

还有我的固定装置:results.yml...

BrickHouse:
 id: 1
 name: Brick House
 description: Squat 500 lbs
 achievable: BrickHouseCriteria (WeightAchievement)

weight_achievements.ym...

 BrickHouseCriteria:
     id: 1
     weight_required: 500
     exercises_id: 1

即使,我无法让它运行,也许从宏观的角度来看,这是一个糟糕的设计问题。我试图做的是有一个包含所有成就及其基本信息(名称和描述)的表。使用该表和多态关联,我想链接到包含完成该成就的标准的其他表,例如,WeightAchievement 表将具有所需的权重和锻炼 ID。然后,用户的进度将存储在 UserProgress 模型中,在该模型中链接到实际的成就(而不是 WeightAchievement)。

我需要单独表格中的标准的原因是因为标准在不同类型的成就之间会有很大差异,并且会在之后动态添加,这就是为什么我没有为每个成就创建单独的模型的原因。

这甚至有意义吗?我是否应该将成就表与特定类型的成就(如 WeightAchievement)合并(所以该表是名称、描述、weight_required、exact_id),然后当用户查询成就时,我只需在我的代码中搜索所有成就?(例如 WeightAchievement、EnduranceAchievement、RepAchievement 等)

4

1 回答 1

13

成就系统通常的工作方式是可以触发大量的各种成就,并且有一组触发器可以用来测试是否应该触发成就。

使用多态关联可能不是一个好主意,因为加载所有成就以运行并测试它们最终可能会成为一项复杂的练习。还有一个事实是,您必须弄清楚如何在某种表中表达成功或失败条件,但在很多情况下,您最终可能会得到一个映射不那么整齐的定义。您最终可能会拥有 60 个不同的表来表示所有不同类型的触发器,这听起来像是一场噩梦。

另一种方法是根据名称、价值等定义您的成就,并拥有一个充当键/值存储的常量表。

这是一个示例迁移:

create_table :achievements do |t|
  t.string :name
  t.integer :points
  t.text :proc
end

create_table :trigger_constants do |t|
  t.string :key
  t.integer :val
end

create_table :user_achievements do |t|
  t.integer :user_id
  t.integer :achievement_id
end

achievements.proc列包含您评估以确定是否应触发成就的 Ruby 代码。通常,这会被加载、包装并最终成为您可以调用的实用方法:

class Achievement < ActiveRecord::Base
  def proc
    @proc ||= eval("Proc.new { |user| #{read_attribute(:proc)} }")
  rescue
    nil # You might want to raise here, rescue in ApplicationController
  end

  def triggered_for_user?(user)
    # Double-negation returns true/false only, not nil
    proc and !!proc.call(user)
  rescue
    nil # You might want to raise here, rescue in ApplicationController
  end
end

该类TriggerConstant定义了您可以调整的各种参数:

class TriggerConstant < ActiveRecord::Base
  def self.[](key)
    # Make a direct SQL call here to avoid the overhead of a model
    # that will be immediately discarded anyway. You can use
    # ActiveSupport::Memoizable.memoize to cache this if desired.
    connection.select_value(sanitize_sql(["SELECT val FROM `#{table_name}` WHERE key=?", key.to_s ]))
  end
end

在您的数据库中拥有原始 Ruby 代码意味着可以更轻松地动态调整规则,而无需重新部署应用程序,但这可能会使测试更加困难。

示例proc可能如下所示:

user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required]

如果你想简化你的规则,你可以创建一些自动扩展的$brickhouse_weight_required东西TriggerConstant[:brickhouse_weight_required]。这将使非技术人员更具可读性。

为了避免将代码放入您的数据库中,有些人可能会觉得这很糟糕,您必须在一些批量过程文件中独立定义这些过程,并通过某种定义传递各种调整参数。这种方法看起来像:

module TriggerConditions
  def max_weight_lifted(user, options)
    user.max_weight_lifted > options[:weight_required]
  end
end

调整Achievement表格,使其存储有关要传入的选项的信息:

create_table :achievements do |t|
  t.string :name
  t.integer :points
  t.string :trigger_type
  t.text :trigger_options
end

在这种情况下trigger_options,是一个序列化存储的映射表。一个例子可能是:

{ :weight_required => :brickhouse_weight_required }

将其结合起来,您会得到一个稍微简化的、不太eval快乐的结果:

class Achievement < ActiveRecord::Base
  serialize :trigger_options

  # Import the conditions which are defined in a separate module
  # to avoid cluttering up this file.
  include TriggerConditions

  def triggered_for_user?(user)
    # Convert the options into actual values by converting
    # the values into the equivalent values from `TriggerConstant`
    options = trigger_options.inject({ }) do |h, (k, v)|
      h[k] = TriggerConstant[v]
      h
    end

    # Return the result of the evaluation with these options
    !!send(trigger_type, user, options)
  rescue
    nil # You might want to raise here, rescue in ApplicationController
  end
end

除非您有一个可以松散地定义触发器测试的记录类型的映射表,否则您通常必须对一大堆Achievement记录进行选通以查看它们是否已实现。这个系统的一个更健壮的实现将允许您为每个成就定义特定的类来观察,但这种基本方法至少应该作为一个基础。

于 2010-08-21T17:54:01.103 回答