简易的条件组分词器

这是一个简单且易用的条件组分词器,提供了c#、c++、python等多种语言的实现和示例,如果你的项目里需要支持多层条件或组合条件之类的机制,那么它将非常适合你

简易的条件组分词器 https://github.com/YaoXuanZhi/condition_group_tokenizer

前言

为了说明这个工具库怎么用,这里以一个游戏项目为例,在这类项目中,条件配置是很大一项被频繁修改的内容,一个通用且可维护性强的条件支持机制在此显得尤为重要,这里提供了一种程序只需要为每类条件做一次机制支持,最终条件配置交由策划童鞋自行组合的配置方式,而且这类配置能够很好兼容excel、csv等格式

需求分析

策划童鞋设计一个功能开启需求,并且罗列了一些例子,如下所示:

系统功能名 开启条件
武器系统 玩家等级达到5级
坐骑系统 玩家等级达到15级
天赋系统 武器系统升到10级【且】开启了坐骑系统
聊天系统 玩家等级达到30级【且】充值过
公会系统 玩家等级达到100级【或】累计登录30天

上述这些条件又应该是怎么配置到配置表上呢,比如在excel表上怎么配置这些条件呢

配置表设计

将罗列的开启条件进行整理,如下所示:

配置格式设计如下:Type-Param1-Param2-Param3

Type(条件类型) Desc(描述) Template(模板) Example(示例)
is_system_open 系统A是否开启 is_system_open-system_id 武器系统是否开启:is_system_open-weapon_sys
system_level 系统A达到B等级 system_level-system_id-need_level 武器系统是否达到10级: system_level-weapon_sys-10
player_level 玩家达到A等级 player_level-need_level 玩家是否达到10级:player_level-10
acc_charge_total 累计充值总额达到A acc_charge_total-need_total 是否累计充值总额已达1000元:acc_charge_total-1000
acc_login_days 累计登录天数达到A acc_login_days-need_days 是否累计登录了30天:acc_login_days-30

各个具体条件已经拆分好了,那么怎么表述它的【且】和【或】关系呢,在此,引入了另外两个特殊符号,如下所示:

符号 作用
&& 完成条件A条件B的且运算
|| 完成条件A条件B的或运算

那么再来到复杂的条件示例上,它们将会简化成如下配置:

  • 描述:玩家等级达到30级【且】充值过 ==> 配置项:player_level-30 && acc_charge_total-1
  • 描述:玩家等级达到100级【或】累计登录30天 ==> 配置项:player_level-100 || acc_login_days-30

最终的配置表格式如下:

系统功能名 注释 开启条件
武器系统 玩家等级达到5级 player_level-5
坐骑系统 玩家等级达到15级 player_level-15
天赋系统 武器系统升到10级【且】开启了坐骑系统 system_level-weapon_sys-10 && is_system_open-mount_sys
聊天系统 玩家等级达到30级【且】充值过 player_level-30 && acc_charge_total-1
公会系统 玩家等级达到100级【或】累计登录30天 player_level-100 || acc_login_days-30

上述条件配置项已经很像我们编程语言里的条件表达式了,比如C++里面的if statement,与之相比我们还差了一个小括号()的功能和逻辑运算符:非的支持,不过一般用不上,可以酌情考虑要不要搞,有了小括号()的支持后,我们可以表达更加复杂的条件了,诸如:

  • 描述:(武器系统升至50级 【且】锻造系统升至30级) 或 玩家充值达到10000元
  • 配置项:(system_level-weapon_sys-50 && system_level-mount_sys-30) || acc_charge_total-10000

给这类复杂条件起个名字,叫做条件组,因为每个小括号包裹住的条件可以当作一个条件分层组,如果完全没有小括号的话,可以看作它有个隐藏的小括号,同策划童鞋沟通好,达成共识

程序设计

上面已经将相关复杂条件描述的条件配置都设计好了,相关测试用例也收集了一些,那么接下来就是提供功能支持来解析条件配置,并且封装好判断接口,再将各个条件类型的支持给加上

程序实现步骤拆解:

  1. 实现一个Tokenizer:解析这个配置项文本,依据其设计好的分割规则,拆分成一个Token数组,然后提取里面的条件块逻辑运算符层级关系,举个简单例子:将 system_level-weapon_sys-50 && system_level-mount_sys-30 解析成一个数组:system_level-weapon_sys-50&&system_level-mount_sys-30
  2. 将ConditionToken进行解析并返回结果:将上述例子里的 system_level-weapon_sys-50system_level-mount_sys-30 进一步解析,并且调用各个系统的数据判断该条件的结果,将结果结合&&逻辑运算符得出结果,可以简化成 result1 && result2

功能实现

参考condition_group_tokenizer 里的Demo示例,重载ProxyCondition()方法,直接进入到步骤2即可

这里贴了Python版的测试用例,其它语言依葫芦画瓢就行

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# coding=utf-8
from condition_group_tokenizer import ConditionGroupTokenizer
import unittest

class PlayerFakeDataComponent:
    '''
    伪造玩家数据
    '''
    def __init__(self) -> None:
        self.buildFakeDatas()

    def buildFakeDatas(self):
        '''
        测试用例可以重载该函数
        '''
        # 已开启的系统数据
        self.systemDatas = {
            "weapon_sys" : {"level":39},
            "mount_sys" : {"level":31},
            }

        # 累计登录天数
        self.accLoginDays = 3

        # 累计充值金额
        self.accChargeTotal = 3

        # 玩家等级
        self.level = 3

class PlayerConditionComponent(ConditionGroupTokenizer):
    """
    玩家条件组件
    """
    def __init__(self, playerDataComponent:PlayerFakeDataComponent) -> None:
        super().__init__()
        self.playerDataComponent = playerDataComponent

    def proxyCondition(self, source:str, isPromt:bool) -> bool:
        elements = source.split("-")

        atomType = elements[0]
        params = elements[1:]

        result = self.checkConditionAtom(atomType, params)
        if isPromt:
            print(f"判断:{atomType}:{params} 结果为:{result}")
        return result

    def checkConditionAtom(self, atomType, params) -> bool:
        """
        根据条件类型来逐个判断
        """

        if atomType == "is_system_open":
            systemId = params[0]
            return systemId in self.playerDataComponent.systemDatas
        elif atomType == "system_level":
            systemId = params[0]
            needSystemLevel = int(params[1])
            if systemId in self.playerDataComponent.systemDatas:
                systemInfo = self.playerDataComponent.systemDatas[systemId]
                return systemInfo["level"] >= needSystemLevel
            return False
        elif atomType == "player_level":
            needPlayerLevel = int(params[0])
            return self.playerDataComponent.level >= needPlayerLevel
        elif atomType == "acc_charge_total":
            needAccTotal = int(params[0])
            return self.playerDataComponent.accChargeTotal >= needAccTotal
        elif atomType == "acc_login_days":
            needAccDays = int(params[0])
            return self.playerDataComponent.accLoginDays >= needAccDays
        else: 
            assert False, f"条件类型:{atomType} 还没支持,请完善相关条件判断逻辑"
        return False

class TestCondtionGroupComponent(unittest.TestCase):
    def __init__(self, methodName: str = "且运算逻辑") -> None:
        super().__init__(methodName)
    def test_and_operator_success(self):
        '''
        测试用例-&&运算符:成功
        '''
        class TestDataComponent(PlayerFakeDataComponent):
            def buildFakeDatas(self):
                # 已开启的系统数据
                self.systemDatas = {
                    "weapon_sys" : {"level":50},
                    "mount_sys" : {"level":60},
                    }

                # 累计登录天数
                self.accLoginDays = 3

                # 累计充值金额
                self.accChargeTotal = 3

                # 玩家等级
                self.level = 3

        source = "system_level-weapon_sys-50 && system_level-mount_sys-30"
        dataComponent = TestDataComponent()
        conditionComponent = PlayerConditionComponent(dataComponent)
        result = conditionComponent.directCheck(source, False)
        self.assertTrue(result, f"判断:{source} 结果为:{result}")

    def test_and_operator_fail(self):
        '''
        测试用例-&&运算符:失败
        '''
        class TestDataComponent(PlayerFakeDataComponent):
            def buildFakeDatas(self):
                '''
                伪造玩家数据
                '''
                # 已开启的系统数据
                self.systemDatas = {
                    "weapon_sys" : {"level":50},
                    "mount_sys" : {"level":29},
                    }

                # 累计登录天数
                self.accLoginDays = 3

                # 累计充值金额
                self.accChargeTotal = 3

                # 玩家等级
                self.level = 3

        source = "system_level-weapon_sys-50 && system_level-mount_sys-30"
        dataComponent = TestDataComponent()
        conditionComponent = PlayerConditionComponent(dataComponent)
        result = conditionComponent.directCheck(source, False)
        self.assertFalse(result, f"判断:{source} 结果为:{result}")

    def test_or_operator_success(self):
        '''
        测试用例-&&运算符:成功
        '''
        class TestDataComponent(PlayerFakeDataComponent):
            def buildFakeDatas(self):
                '''
                伪造玩家数据
                '''
                # 已开启的系统数据
                self.systemDatas = {
                    "weapon_sys" : {"level":50},
                    "mount_sys" : {"level":29},
                    }

                # 累计登录天数
                self.accLoginDays = 12

                # 累计充值金额
                self.accChargeTotal = 3

                # 玩家等级
                self.level = 3

        source = "is_system_open-pet_system || (acc_charge_total-100 || acc_login_days-10) || player_level-5"
        dataComponent = TestDataComponent()
        conditionComponent = PlayerConditionComponent(dataComponent)
        result = conditionComponent.directCheck(source, False)
        self.assertTrue(result, f"判断:{source} 结果为:{result}")

    def test_or_operator_fail(self):
        '''
        测试用例-&&运算符:成功
        '''
        class TestDataComponent(PlayerFakeDataComponent):
            def buildFakeDatas(self):
                '''
                伪造玩家数据
                '''
                # 已开启的系统数据
                self.systemDatas = {
                    "weapon_sys" : {"level":50},
                    "mount_sys" : {"level":29},
                    }

                # 累计登录天数
                self.accLoginDays = 3

                # 累计充值金额
                self.accChargeTotal = 3

                # 玩家等级
                self.level = 3

        source = "is_system_open-pet_system || (acc_charge_total-100 || acc_login_days-10) || player_level-5"
        dataComponent = TestDataComponent()
        conditionComponent = PlayerConditionComponent(dataComponent)
        result = conditionComponent.directCheck(source, False)
        self.assertFalse(result, f"判断:{source} 结果为:{result}")

if __name__ == '__main__':
    unittest.main()

结尾

在游戏项目里,我们仅需实现一次通用条件组件模块,然后在奖励发放、任务触发&完成、成就事件达成等业务模块上,都调用这个通用组件来判断条件是否满足

当然,真正要集成到项目里,还需要考虑到非法输入等情况,配置检查之类的事情,这块没法偷懒

后记

  • 为UE定制一个条件组插件,时间充足的话,还可以支持上DataTable

参考资料

0%