Rails 微信大转盘制作过程及算法分析

wedxt · 2015年05月27日 · 最后由 gazeldx 回复于 2015年05月28日 · 6095 次阅读

在开发维斗喜帖的微转盘时候,研究了一下转盘应用,不过各种算法都相对复杂了,当时制作维斗喜帖微转盘只有一个目的:用户只需要设定奖项,填写抽奖人数,获奖人数,整个转盘游戏就搞定,简单轻松傻瓜化非严肃性质的微游戏定位。

本实例的代码适用于以下约定的需求场景:

奖项设置:

转盘为 8 格,可设置 6 个奖项,剩余 2 格为未中奖区域 一等奖至六等奖的中奖比例为:1:2:5:10:12:20,即如果 [奖品总数] 设置为:50 个,预计抽奖人数 100 人,则一等奖有 1 个,六等奖有 20 个。 若要扩大抽奖范围,则可以将 [奖品总数] 增加。

需要准备的素材:

第一张为转盘图,大小为:1032x1032px。下载转盘模版(PSD 格式,PNG 格式)http://www.wedxt.com/post/zhuanpan。 第二张为指针图,大小为:256x357px。请点击这里下载指针模版。

制作好后的预览效果:(扫描二维码体验)

前端 JS 代码:

<script src="/assets/jquery.js"></script>
  <script src="/assets/bootstrap.js"></script>
  <script type="text/javascript">
    var extendUrl = 'http://localhost:3000';
    var getPrizeUrl = 'http://localhost:3000/zhuanpan/23/h';
    var commentUrl = 'http://www.example.com'
    var running = false,
    count = 0;

    //随机生成数字3或者7: 未中奖情况
    function rand() {
      var text = "";
      var possible = "37";
      text += possible.charAt(Math.floor(Math.random() * possible.length));
      return text;
    }

    //转盘css3动画
    function setDegree($obj, deg) {
      $obj.css({
        'transform': 'rotate(' + deg + 'deg)',
        '-moz-transform': 'rotate(' + deg + 'deg)',
        '-o-transform': 'rotate(' + deg + 'deg)'
      });
    }

    //转盘转动开始
    //params: offset 0-7,代表需要停到的奖项,由后端传入
    function rotate(offset) { 
      console.log(offset);
      var $tar = $('#outer'), 
      i, 
      cnt = 100,
      //用做ratio的索引(10-29)
      total = 0,
      //记录上一次的变化结果
      ratio = [],
      //存放角度的变化比例,制造快慢过渡效果
      amount = 18 - (0.45 * offset); //每次每多出45/200=0.225度,200次就多偏转45度
      ratio[1] = [0.2, 0.4, 0.6, 0.8, 1, 1, 1.2, 1.4, 1.6, 1.8];
      ratio[2] = [1.8, 1.6, 1.4, 1.2, 1, 1, 0.8, 0.6, 0.4, 0.2];
      for (i = 0; i < 100; i++) { //100次50ms的间隔
        setTimeout(function() {
          //计算每次偏转增量,对应阶段的增减比例最终造成快慢变化
          var deg = amount * (ratio[String(cnt).substr(0, 1)][String(cnt).substr(1, 1)]);
          setDegree($tar, deg + total); //改变偏转
          total += deg; //记录
          cnt++; //依据次数用作ratio的索引,这里用到了闭包不能使用i
        },
        i * 25);
      }
      setTimeout(function() {
        if (offset == 3 || offset == 7) {
          window.sessionStorage.setItem('prizename', '没中奖');
          $('#myModal').modal('show');
          $('#word').html("<p>加油哦,亲!</p>");
          $('#goShop').text('点击这里');
          $('#goShop').click(function() {
            location.href = extendUrl;
          });
        }
      },
      100 * 25 + 250);
    }

    //绑定事件,点击指针开始
    function actRotate() {
      if (running) 
      return;

      if (count >= 1) {
        $('#myModal').modal('show');
        $('#word p').text('亲,每人只能参加一次哦!');
        $('#goShop').text('点击这里');
        $('#goShop').click(function() {
          location.href = extendUrl;
        });
        count = 1;
        return
      }

      if (window.sessionStorage.getItem('prizename') != null)  {
        $('#myModal').modal('show');
        $('#word p').text('亲,你不能再参加本次活动了喔!下次再来吧^_^');
        $('#goShop').text('点击这里');
        $('#goShop').click(function() {
          location.href = extendUrl;
        });     
        return
      }

      $.ajax({ 
        type: "GET",
        url: getPrizeUrl,
        dataType:  "json",
        success:  function(data) {
          console.log(data);
          if (data.status == 0) { //活动还未开始
            console.log('活动还未开始');
            $('#myModal').modal('show');
            $('#word p').text('亲,活动还未开始啦!');
            $('#goShop').text('点击这里');
            $('#goShop').click(function() {
              location.href = extendUrl;
            });
          } else if (data.status == 1) { //活动进行中: 抽奖并中奖
            console.log('活动进行中:中奖');
            window.sessionStorage.setItem('goods_id', data.goods_id);   
            rotate(data.Coupon.grade);
            setTimeout(function() {
              window.sessionStorage.setItem('prizename', data.message + '');
              $('#myModal1').modal('show');
              $('#word1').html("<p>恭喜你,获得了" + data.message + "</p>");
              $('#goNext').click(function() {
                location.href = commentUrl;
              });
            },
            100 * 25 + 250);
          } else if (data.status == 2) { //活动进行中:抽奖未中奖
            console.log('活动进行中:抽奖未中奖');
            var text = rand();
            text = text.toString();
            rotate(text);
          } else if (data.status == 3) { //奖品已经领完
            console.log('奖品已经领完');
            $('#myModal').modal('show');
            $('#word p').text('今日奖品已经领完,明天继续哦!');
            $('#goShop').click(function() {
              location.href = extendUrl;
            });
          } else if (data.status == 4) { //活动结束
            console.log('活动结束');
            $('#myModal').modal('show');
            $('#word p').text('亲,活动已经结束啦!');
            $('#goShop').text('点击这里');
            $('#goShop').click(function() {
              location.href = extendUrl;
            });
          }                
          running  =  false;
          count++;            
        },
        error:  function() {
          $('#myModal').modal('show');
          $('#word').html("<img src='aa.png' class='fail' id='tsImg'>");
          $('#goShop').text('获得帮助');
          $('#goShop').click(function() {
            location.href = extendUrl;
          });
          prize = null;
          running = false;
          count = 0;            
        },
        timeout:  1000        
      });
    }    
  </script>

在这 JS 代码中,我们看到,需要 Ajax 调用服务器返回 JSON 数据:http://localhost:3000/zhuanpan/23 服务器端的代码如下:

#大转盘需要调用的action: 返回json数据,给js调用
#params: site_page_id, key
def zhuanpan_json
  # status 值:
  # 0 -> 亲,活动还未开始啦!
  # 1 -> 活动进行中: 抽奖并中奖
  # 2 -> 活动进行中:抽奖未中奖
  # 3 -> 今日奖品已经领完,明天继续哦!
  # 4 -> 亲,活动已经结束啦!
  @site_page = SitePage.find(params[:id])

  #状态获取
  status = case SitePageKeystore.value_for(@site_page, 'open_toggle')
  when '活动进行中'
    2
  when '活动未开始'
    0
  when '活动已结束'
    4
  else
    2
  end

  #获取用户设定的奖品总数
  prize_obj = SitePageKeystore.get(@site_page.id, 'prize_count')
  prize_count = prize_obj.value.to_i || 50

  #获取预计抽奖人数
  person_obj = SitePageKeystore.get(@site_page.id, 'person_count')
  person_count = person_obj.value.to_i || 100

  #计算中奖概率
  rate = prize_count/(person_count + 0.1)

  #获取已抽奖次数
  zhuan_obj = SitePageKeystore.get(@site_page.id, 'zhuan_count')
  if zhuan_obj.nil?
    key = CommonKey.get('zhuan_count')
    CommonKey.put('zhuan_count', 0) if key.nil?
    SitePageKeystore.put(@site_page.id, 'zhuan_count', 0)
    zhuan_obj = SitePageKeystore.get(@site_page.id, 'zhuan_count')
  end
  zhuan_count = zhuan_obj.value.to_i
  SitePageKeystore.put(@site_page.id, 'zhuan_count', zhuan_count + 1)

  status = 4 if zhuan_count >= person_count #次数已经抽完

  #获取中奖次数
  coupon_obj = SitePageKeystore.get(@site_page.id, 'coupon_count')
  if coupon_obj.nil?
    key = CommonKey.get('coupon_count')
    CommonKey.put('coupon_count', 0) if key.nil?
    SitePageKeystore.put(@site_page.id, 'coupon_count', 0)
    coupon_obj = SitePageKeystore.get(@site_page.id, 'coupon_count')
  end
  coupon_count = coupon_obj.value.to_i

  status  = 3 if coupon_count >= prize_count && status != 4 #奖品已完

  # 奖项数组 
  # 是一个二维数组,记录了所有本次抽奖的奖项信息, 
  # 其中id表示中奖等级,prize表示奖品,v表示中奖概率。 
  # 注意其中的v必须为整数,你可以将对应的 奖项的v设置成0,即意味着该奖项抽中的几率是0, 
  # 数组中v的总和(基数),基数越大越能体现概率的准确性。 
  # 本例中v的总和为100,那么平板电脑对应的 中奖概率就是1%, 
  # 如果v的总和是10000,那中奖概率就是万分之一了。 

  # goods_id: 
  # 8个值分别对应8格转盘的8个格子,其中3,7格为默认未中奖格,其他格为有奖格
  # 1 -> 奖品1
  # 2 -> 奖品2
  # 3 -> 未中奖
  # 4 -> 奖品3
  # 5 -> 奖品4
  # 6 -> 奖品5
  # 7 -> 未中奖
  # 8 -> 奖品6
  failed_count = (50/rate).to_i - 50 #预设中奖个数为50, 则根据中奖概率计算出未中奖个数
  prize_hash = {   
    0 => {'id'=>1,'prize'=> SitePageKeystore.value_for(@site_page, 'good1'),'v'=>1},   
    1 => {'id'=>2,'prize'=> SitePageKeystore.value_for(@site_page, 'good2'),'v'=>2},  
    2 => {'id'=>8,'prize'=> SitePageKeystore.value_for(@site_page, 'good3'),'v'=>5},
    3 => {'id'=>4,'prize'=> SitePageKeystore.value_for(@site_page, 'good4'),'v'=>10},  
    4 => {'id'=>5,'prize'=> SitePageKeystore.value_for(@site_page, 'good5'),'v'=>12},  
    5 => {'id'=>6,'prize'=> SitePageKeystore.value_for(@site_page, 'good6'),'v'=>20},
    6 => {'id'=>3,'prize'=> '没有抽中,感谢参与','v'=> failed_count/3},
    7 => {'id'=>7,'prize'=> '没有抽中,没准下次能抽到哦','v'=> failed_count/3*2},  
  }

  # 每次前端页面的请求,PHP循环奖项设置数组, 
  # 通过概率计算函数get_rand获取抽中的奖项id。 
  # 将中奖奖品保存在数组$res['yes']中, 
  # 而剩下的未中奖的信息保存在$res['no']中, 
  # 最后输出json个数数据给前端页面。 
  arr = {}
  prize_hash.each_pair do |key, val|
    arr[val['id']] = val['v']
  end
  rid = get_rand(arr) #根据概率获取奖项id
  coupon = prize_hash[rid - 1] #抽中的奖项
  if status == 2 && ![3, 7].include?(coupon['id']) #中奖了
    SitePageKeystore.put(@site_page.id, 'coupon_count', coupon_count + 1)
    status = 1
  end

  if status == 1 #中奖了
    render json: {status: status, goods_id: coupon['id'], message: coupon['prize'], Coupon: {grade: coupon['id']}}
  else #没中奖
    render json: {status: status}
  end
end

# 
#  经典的概率算法, 
#  $proArr是一个预先设置的数组, 
#  假设数组为:array(100,200,300,400), 
#  开始是从1,1000 这个概率范围内筛选第一个数是否在他的出现概率范围之内,  
#  如果不在,则将概率空间,也就是k的值减去刚刚的那个数字的概率空间, 
#  在本例当中就是减去100,也就是说第二个数是在1,900这个范围内筛选的。 
#  这样 筛选到最终,总会有一个数满足要求。 
#  就相当于去一个箱子里摸东西, 
#  第一个不是,第二个不是,第三个还不是,那最后一个一定是。 
#  pro_hash = {id => v, 1 => 1, 2 => 30} 
def get_rand(pro_hash)
    result = nil
    #概率数组的总概率精度  
    pro_sum = pro_hash.values.inject{|sum,x| sum + x }
    #概率数组循环
    pro_hash.each_pair do |key, pro_cur|
      rand_num = 1 + rand(pro_sum)
      if rand_num <= pro_cur
        result = key
        break
      else
        pro_sum -= pro_cur
      end
    end
    result
end

中奖提示:

更多案例,可以参考这里:http://www.wedxt.com/templates/12

我用的 jquery-rotate 来控制图片旋转,然后抽奖用的最笨的办法

prizes = {"一等奖" => 5, "二等奖" => 10, "三等奖" => 20}
array = []
prizes.each do |k, v|
  v.times { array << k }
end
prize = array[rand(0..array.length-1)]

http://www.wedxt.com/sites/19387/preview 这里的标题栏是怎么做得?上下滑动就会跟着变化

非常值得参考和学习的一段程序,感谢分享。 以后可能用得到。 何不在 github 上分享成一个项目呢?还能积累点精气。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号