权重工具学习笔记

工具类

游戏涉及概率场景配置经常需要用到权重。一个通用的权重工具可以减少编程难度和重复代码。


public class RatioUtils {
    /*
    *   权重随机接口
    */
    public interface IRatio {

        // 权重值
        int weight();
    }

    // 全额随机,必然命中一个
    public static <T extends IRatio> T random(List<T> radios) {
        return random(ratios,getTototalWeight(ratios));
    }

    /**
    *   根据传入的总权重随机:
    *    1.当ratios的所有权重之和小于传入的总权重,则可能出现为命中
    *    2.当ratios的所有权重之和大于传入的总权重,则可能出现ratios尾部元素无法命中
    */
    public static <T extends IRatio> T random(List<T> ratios, int totalWeight) {
        return baseRandomOne(ratios, totalWeight);
    }

    /*
    *   随机指定数量的元素,所有元素可重复命中
    *
    */
    public static <T extends IRatio> List<T>  randomList(List<T> ratios, int count) {
        return baseRandomList(ratios, count, false);
    }

    // 随机指定数量的元素,所有元素最多命中一次
    public static <T extends IRatio> List<T> randomListNoRepeat(List<T> ratios, int count) {
        if(ratios.size() <= count>) {
            // 随机列表数量少于需求数量时,直接返回随机列表所有元素
            return new ArrayList<>(ratios);
        }
        return baseRandomList();
    }

    // 基础随机一个
    public static <T extends IRatio> T baseRandomOne(List<T> ratios, int totalWeight) {
        if(ratios.isEmpty()) {
            return null;
        }
        if(totalWeight <= 0>) {
            throw new IllegalArgumentException("totalWeight is"+0);
        }
        int hit = RandomUtils
    }

    // 基础随机多个
    private static <T extends IRatio> List<T> baseRandomList(List<T> all, int count, booleam noRepaeat) {
        if(noRepeat) {
            all = new LinkedList<>(all);
        }
        List<T> hitList = new ArrayList<>(count);
        int totalWeight = getTotalWeight(all);
        for(int i=0; i<count; i++) {
            T t = baseRandomOne(all,totalWeight);
            if(t == null) {
                coninue;
            }
            hitList.add(t);
            if(noRepeat) {
                all.remove(t);
                totalWeight -= t.wegight();
            }
        }
        return hitList;
    }

    // 获取随机列表的总权重
    public static <T extends IRatio> int getTotalWeight(List<T> ratios) {
        int totalWeight = 0;
        for(T t : ratios) {
            int weight = t.weight();
            if(weight <= 0) {
                throw new IllegalArgumentException(String.format("wegiht %d is <= 0", weight));
            }
        }
        return totalWeight;
    }

    // 权重随机,一维数组配置方式: id,权重,id,权重,id,权重
    public static int random(int[]  rewards) {
        int total = 0;
        for(int i=0; i<rewards.length; i+=2) {
            total +=rewards[i+1];
        }
        int hit = RandomUtils.nextInt(total);
        for(int i=0; i<rewards.lenght; i+=2) {
            int ratio = rewards[i+1];
            if(hit < ratio>) {
                return rewards[i];
            }
            hit -= ratio;
        }
        throw new RuntimeException("error random hit! total"+ total + ",hit"+ hit);
    }

    // 剔除权重为0的元素
    public static <T extends IRatio> List<T> filterWightNot0(List<T> ratios) {
        if(ratios.isRmpty()) {
            return Collections.empty();
        }
        List<T> all = new ArrayList<>(ratios.size());
        for(T ratio : ratios) {
            if(ratio.weight() > 0 ) {
                all.add(ratio);
            }
        }
        return all;
    }
}

使用方式

  1. 使用时先让需要权重随机的实体实现IRatio接口,重写weight()方法。例如需要根据权重随机选取配置类时:

     public XXxResource implements RatioUtils.IRatio {
    
         // ......
    
         @Override
         public int weight() {
             return weight;
         }
     }
    
  2. 在需要权重随机的地方

     RatioUtils.random(xxxxResourceList)
    

服务开发原则

  1. 通信packevent事件、corn定时任务必须在facade层作为开始。
  2. service层不允许作为被其他模块调用。只能被自己模块的facade调用。manager层允许其他业务模块调用。如果涉及到多线程manager自己考虑自身的线程安全。
  3. 可以把用用的(其他模块也会用到的,确定只有自己会用到的条件没必要做抽象了,避免代码不直观)条件设计成condition。根据策划接收程度考虑用json配置或者conditionResource配置
  4. 跨模块资源消耗、策划需要统一配置的资源必须抽象出Consumes去消耗。
  5. 跨模块资源发放、策划需要统一配置的资源发送不允许直接修改领域对象。必须走reward方便管理和记录日志。
  6. chooser作为条件选择器。可以抽象出一些统一的筛选规则。比如条件筛选、概率筛选。
  7. 不允许在代码中拼接文本内容。
  8. 玩家自身的任务采用的是玩家的accounthashcode来作为任务分发。公共模块例如交易、公会等业务需要模块自身维护加锁。使用到锁的模块必须与项目主程沟通协商。`AtomicXX.compareAndSet()解决不了代码块的问题,如果觉得可以用和主程讨论以后再决定。
  9. 实践证明,程序参与配置表配置然后策划改修数值比直接掉给策划一张空表开发效率会高很多。
  10. 每一张Resource配置表。必须要包含一个sheet配置说明,并且不断维护更新。这个说明的起草通常由服务端开发人员发起。然后各方维护。
  11. 配置表映射的Resource类中为了防止配置表对象被无意中修改。应当不包含set*方法。
  12. 每一模块,开发完成后以后,必须用findbugs和阿里编程规范插件扫面。高级别的错误必须修正。
  13. 设计一个活动系统的时候。无论策划文档是否有明确指出,都应该将是否热更新考虑进去。
  14. 项目中不允许出现System.out.println(),统一使用SystemOut.println()代替。
  15. 服务端开发应视为客户端已经被破解。所以设计到安全和收益相关的逻辑,必须做好严格的验证。不得依赖客户端的消息。
  16. 所有开发中的常量都必须使用配置的方式实现,不允许将常量定义在代码中,可以配置在ConfigValue.xml中。
  17. 所有代码中需要被处理的错误,都应该用Logger.error记录下来,及时查看error日志并解决。
  18. 代码中的return,仔细思考为什么需要中断业务,不应该什么都不做直接return。
    协议处理时需要return的地方,应该选择以下三种方式
    a 客户端没法预判断,需要服务器来判断的,提示对应错误信息

    `throw ManagedException.valueOf(result.getCode())`
    

    b 客户端可以预检验的情况下发过来的 没有被检测的请求 统一返回请求非法就行

    `throw ManagedException.valueOf(MessageCode.ERROR_MSG)`
    

    c 其他不属于上面,只能直接return的,用Logger.error记录日志

  19. 必须安装提交日志的插件,提交日志需要遵循规定的格式,插件中的类型、标题、说明为必填项

  20. 提交代码时必须勾选以下选项
  • Alibaba Code idelines
  • Reformat code
  • Optimize import
  • Perform code annlysis
  • Check TODO

有失效的标记

public class AgingMark<K> {
    //标记map
    private final ConcurrentHashMap<K,Long> markMap = new ConcurrentHashMap(1);

    // 刷新标记事件
    public void refreshMark(K k, int time, TimeUnit timeUnit) {
        markMap.put(k, System.currentTimeMillis() + timeUnit.toMillis(time));
    }
    // 移出标记
    public boolean reomveMark(K k) {
        return markMap.remove(k) != null;
    }
    // 检测使用拥有指定标记
    public boolean hasMark(K k) {
        Long time = markMap.get(k);
        if(time == null) {
            return false;
        }
    }
    // 是否没有任何标记
    public boolean isEmpty() {
        return markMap.isEmpty();
    }
    // 是否没有任何标记
    public void tryRemoveExpire(Consumer<K> consumer) {
        if(markMap.isEmpty()) {
            return;
        }
        long now = System.currentTimeMillis();
        markMap.entrySet().removeIf(k -> {
            // 注意,这里是先执行回调后后移出
            if(k.getValue() < now) {
                if(consumer != null) {
                    consumer.accept(k.getKey())
                }
                return ture;
            } else {
                return false;
            }
        });
    }

    // 获取指定key的过期时间
    public long getExpireTime(K k) {
        Long time = markMap.get(k);
        if(time == null) {
            return 0;
        }
        long expire = time - System.currentTimeMillis();
        if(expire <= 0) {
            markMap.remove(k);
            return 0;
        }
    }

    // 当前拥有的所有key
    public Set<K> returnKeysAndClear() {
        Set<K> keys = new HashSet<>(markMap.keySet());
        markMap.clear();
        return keys;
    }
    // 清除所有标记
    public void clear() {
         markMap.clear();
    }

    // 遍历所有未过期的标记
    public void walkAll(BiConsumer<K,Long> walker) {
          markMap.keySet().foreach(
              k ->  {
                  long expire = getExpireTime(k);
                  if(expire > 0) {
                      walker.accept(k, expire);
                  }
              }
          );
    }

    // 注意,该接口返回的数量,只是当前标记集合数据的数量,不代表当前还未过去的标记数量
    public int size() {
        return markMap.size();
    }

}