简单兑换过程的设计和数据结构讨论

一 需求简要描述

游戏中物品道具,同类不同物兑换过程。
道具中,有某种具有不同等级的物品,比如绿色宝石,蓝色宝石,紫色宝石,金色宝石,红色宝石,等等。它们之间具有一定的兑换关系,即,绿色宝石40个可以兑换一个蓝色宝石,蓝色宝石20个可以换一个紫色宝石,紫色宝石5个可以换一个金色宝石等,宝石兑换过程不能逆向兑换。绿色=》蓝色=》紫色=》金色=》红色是等级递推关系。要求,在兑换过程中可以自动计算跨越等级的宝石之间的兑换过程。比如:足够绿色也可以换紫色宝石。

40绿色 =》 蓝色
20蓝色 =》 紫色
…………
800绿色 =》 紫色
…………

在游戏中,不同颜色的宝石具有不同的作用。比如,绿色宝石用作镶嵌护手装备;蓝色宝石用作镶嵌靴子等等。

我们的问题就是实现一个兑换过程,能够通过计算宝石之间的数据对应关系,扣除绿色宝石并加成蓝色宝石。

二 分析需求中的可变因素

相信很容易就能得到不同颜色前面的那个数字会是一个可变的因素,我们会将其作为宏替换在程序中。另外不能逆向递推等级的兑换宝石,我们可以讲其设置为一个线性结构,是最节省空间,并且容易计算的。

三 实现过程

(近期一直在使用Lua,所以使用Lua实现)

遵循一定的编程规范。首先,设计出程序需要的数据结构;其次,根据数据结构得到程序的结构。
我们需要一个线性结构,描述逐级兑换过程的数据:

Exchange_table = {
    [“green”] = 40,
    [“blue”] = 20,
    [“violet”] = 5,
    [“golden”] = 2,
    [“red”] = 1,
}

数组结构

记录不同等级的宝石兑换到上一个等级的兑换数量,这样,数字需要修改时,不需要改变程序中逻辑。
这里记录的是单位兑换数量,兑换一个上层宝石需要的下层宝石数量,即如果给出80个绿色宝石,需要得到2个蓝色宝石。
我们为了便于比较和计算,将宝石的颜色值设置为id,记录到另一个表里。

Stones = { 1 , 2 , 3, 4 , 5 }
--对应于:1:绿宝石;2:蓝宝石;3:紫宝石…………

Exchange = { 40 , 20 , 5 , 2 , 1} 
--同上面的兑换数量值,跨越等级兑换的时候,需要自动计算扣除宝石数量,以及加成宝石数量

函数实现兑换过程:

----------------------------------------------------------------------------------
-- 计算宝石兑换的单位数量:一个toid需要多少fromid
-- 参数:fromid , toid
-- 返回值:from_num_need  或者false是逆向兑换  或者1是和自己换
function get_from_count( fromid , toid )
  local from_num_need = 1

  if fromid > toid then
    return false  --不可以逆向兑换
  elseif  fromid == toid then
    return from_num_need
  end

  for i = fromid , toid-1 , 1 do  
--由于for循环 exp1 等于 exp2 也要执行一次body,所以toid-1
    from_num_need = from_num_need * Exchange[i]
  end
  return  from_num_need
end

----------------------------------------------------------------------------------
--  计算可以兑换几个高级宝石
--  参数:from_num_need(兑换单位) , from_num(提供数量)
--  返回值:第一个:0:能兑换0个(提供数量不够);1:能兑换一个;2:能兑换2个......
--			第二个:余数,表示提供的低等级兑换数量大于兑换需要的低等级数量。
function get_num_toid( from_num_need , from_num )
  if from_num_need > from_num then
    return 0
  end

  return  math.floor( from_num / from_num_need ) , from_num % from_num_need
end

那么接下来就按部就班,根据得到的能够兑换高级宝石的数量,以及消耗低级宝石数量。
举例:(1607个绿色宝石,换紫色宝石)==》换2个紫色宝石

消耗低级宝石数量: get_num_toid( get_from_count( 1, 3 ) , 1607 ) * get_from_count( 1, 3 )
加成高级宝石数量: get_num_toid( get_from_count( 1, 3 ) , 1607 )

将需要兑换的高级宝石数量和低级宝石数量的计算过程:第一步得到单位兑换数量,第二步计算实际兑换数量。

四 反思实现过程

目前看来过程并没有什么大的漏洞和设计偏差,差不多是按照需求完成。然而,这段代码中间有一个死穴:首先需求中明确要求不能逆向兑换,所以代码设计也遵从不能逆向兑换,那么有一天,产品策划跑来说:那个,为了使我们的玩家更加自由,我们决定将规则改为双向自由兑换,并且升级兑换和降级兑换的数据最好使用表格(CSV)来控制。除了一顿抱怨和争吵之后,我们反观自己,这段程序的灵魂——数据结构部分,设计的是否足够灵活,能够完美适应变化。很明显不能适应变化。
这个Exchange表本身是具有方向的表,即:下标1的元素记录的是兑换到下标2表示的宝石需要花费的数量,下标2记录的是下标3宝石的数量,以此类推。那么什么地方记录下标2记录下标1宝石花费的数量?所以,问题就在这里,Exchange表是单向的。

需要设计出,更加灵活的数据结构,可能更应该关心的是我们要做的是什么,我们要做的是:兑换,换句话说,就是 扣除一种东西,加成另一种东西。
那么,我们需要的可能不是一个线性的数据结构。那么,树和图,谁更合适呢?个人认为是图,因为,树是节点选择很多,但方向性依然存在。

得到,使用图结构描述这个问题,那么再次改写上面程序。抽象一下问题:扣除谁,扣除多少,加成谁,加成多少。那么图的节点from_id和to_id就是这句话里面的2个“谁”,这两个节点之间的边上的权值就是2个“多少”。

图结构

五 再次实现过程

Stones = { 1 , 2 , 3, 4 , 5 }
Exchange_2 = {
    [ 1 ] = { [ 2 ] = { 40 , 1 },  [ 3 ] = { 800 , 1 },  [4] = { 4000 , 1},  [5] = {8000 , 1} },
    [ 2 ]= { [ 1 ] = { 1 , 40 },  [ 3 ] = { 20 , 1 },  [4] = { 100 , 1},  [5] = {200 , 1} },
    [ 3 ]= { [1] …………},
    …………
}

上面的数据内容是用Lua中的table实现的,那么,可以使用CSV以表格形式填写之后,专门写一个功能加载为Lua的table类型,方便程序外部修改数值。

根据上面的数据结构,我们的代码,可能更加清晰并且不易变动了。

再次实现:

----------------------------------------------------------------------------------
--  根据表格中数据返回本次兑换的单位数据
--  参数: from_id , to_id , provide , flag
--  返回值: from_dec_num , to_add_num  若为0,则表示上行provide不够单位交换
function get_add_dec_num( from_id , to_id , provide , flag)
  if not Exchange_2  then return end

  -- 表里没有本次兑换的数据
  if not Exchange_2 [from_id][to_id] then return end

  --得到本次兑换 的 单位比率
  local from_num = Exchange_2 [from_id][to_id].from_num
  local to_num = Exchange_2 [from_id][to_id].to_num

  --计算本次兑换 实际倍数
  local times = 0
  if flag == 'from' then -- provide描述 from_num
    times = math.floor( provide / from_num )
  elseif flag == 'to' then   -- provide描述 to_num
    times = math.floor( provide / to_num )
  end

  return  (from_num * times) , (to_num * times)
end

调用该函数得到的数字就是一次兑换需要扣除的“多少”,和加成的“多少”。
举例:(1607个绿色宝石,换紫色宝石)==》换2个紫色宝石

local dec_num,add_num =  get_add_dec_num( 1 , 3 , 1607 , ‘from’)

到这里,这个需求想要描述的问题,也已经解决了,应对本问题可能出现的变更也考虑到了,兑换率(单位兑换数量)在表中变化即可。当产品策划需要变化时,比如:正向兑换和逆向兑换的数据想要不一致,就可以很舒服的说:自己改表去。

六 最后总结

  • 首先,需要说的是,程序的设计要和数据结构的设计结合起来。
  • 其次,分析需求的时候,着重关注可变因素,更重要的是,虽然明说的要求,也要再三思量,不要绝对作为硬规则。
  • 最后,关注需求本质,而不是需求的描述,主动应对需求变更。

==========================================================================================================

posted @ 2014-08-19 23:18  Appleegig  阅读(688)  评论(0编辑  收藏  举报