因此,Dice现在位于GitHub中(3/3)

我注意到它仍然是2017年,并且我仍在努力使GitHub发挥作用。 我从一篇有关Dice类的文章开始,然后是一篇关于如何测试Dice并添加解析器的文章。 这是第三篇也是最后一篇文章,将所有Dice内容都放入GitHub。

确定许可证

我研究了适用于Dice的软件许可,或更广泛的说是RolePlayingCore,并选择了MIT许可 因为它提供了很多开放性和灵活性,同时又避免了疯狂的打击。 在担任开发人员的那段时间里,我已经处理了好几个开放和封闭源代码许可库。 我还与我工作过的每家公司的法律部门和法律顾问打交道,而且我也看到了自己的诉讼和违约情况。 我了解到有些许可证和条款值得使用,有些则不行。 一般来说,如果许可证的长度超过一页,那么很可能会在以后发生坏事。 而且他们经常会。

在这种情况下,值得使用此许可证,因此我为RolePlayingCore选择了该许可证。

持续集成CI脚本

一旦有了代码并提交了初始README.md ,我便转到了多个分支并进行了持续集成。 好了,现在,仍然有两个分支,即masterdevelopment

至于持续集成,我选择Travis CI是因为它支持iOS,并且它是选择列表的顶部。 不幸的是,我挣扎了一下。 有时候我可以成为一个真正的白痴。 开启它之后,我进入了其中一种匆匆忙忙的模式,“它应该工作”,然后……只是……没有,一遍又一遍。

我的.travis.yml文件在第一个实际上没有构建任何内容的提交到实际成功构建的提交之间有12个提交失败的编辑。 有时我会陷入这种模式,在这种模式下我会无意识地跳动一个问题,直到最终让步。 经过的时间是三天,主要是因为我有一份日常工作。 实际上只浪费了我一两个小时。 在这种情况下,我不知道“成功”是什么样子,我一直在猜测,有时甚至是疯狂,直到我最终决定检查其他GitHub iOS存储库以寻求帮助。 我的困难实际上并没有那么糟糕,主要是:(1)正确设置Xcode版本,(2)正确选择方案名称,(3)正确设置模拟器名称。 我只是把它们弄对了而愚蠢。

实际开始工作时的情况如下:

 语言:Objective-C 
  osx_image:xcode8.3 
 脚本: 
  -设置-o pipefail 
  -travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme“ RolePlayingCore”-目标“ platform = iOS Simulator,name = iPhone 7”测试构建  xcpretty 

这是一个开始。 但尚未完成。 完成将包括设备测试等等。

我可能会考虑做的一件事是设置本地Travis,这样我就可以在推出更改之前对更改进行限定。

我尚不知道如何做的一件事是确保代码覆盖率保持在100%,如果低于或低于某个阈值,则失败。 读者 ,您知道该如何设置吗?

管理环境

每当软件开发达到集成水平时,对第三方的依赖就变得非常重要,可以进行检查和管理。 在这种情况下,没有其他库,但是有我用来构建的编译器和我用来测试的模拟器。 都给我带来麻烦。

我的Adventures中未能指定.travis.yml文件的事情之一是Travis osx_image中的现有错误,特别是Xcode 8.2模拟器设备-它们由于安装了重复项而无法工作-所以我确保添加特拉维斯(Travis)发布到我的关注列表。 而且,可以肯定的是,它使用Travis Xcode 8.3图像得到了解决,这是我移至Xcode 8.3的原因之一。 我迁移到Xcode 8.3的另一个原因是因为Apple修复了Swift中的一个错误。

我知道,对吧? 苹果修复错误? 这与mathOperators地图有关。 如果我有3个以上,编译器将死于“ expression-to-complex”。我真的希望所有运算符 (通常是4个)加上“ x”作为“ *”的变体,以便乘法,所以我我很高兴Apple在编译器中修复了一些使我拥有它们的东西。 关于自我的注意事项:自我,请始终提交Radar

进行代码共享

目前为止,我仅打算将存储库作为iOS框架使用。 我尚未准备好与CocoaPods等人打交道。 我正在认真考虑为Swift软件包做出贡献,以帮助它发展到支持框架的地步,但是我现在没有足够的带宽来娱乐它。 我还想开发一个或三个示例客户端应用程序,因此在去那里之前,我对公开发布的意义(在Swift导出的意义上)有更好的了解。

我的第一个GitHub“问题”

我想使用GitHub Issues功能提出的第一个问题是初始骰子解析器。 由于它很烂,所以我采取了简单的方法,这就是我可以在很短的时间内生产出的东西。 我因此在GitHub中描述了这个问题:

DiceParser当前的要求是使用掷骰,修饰符,投掷掷骰和复合骰子(多个骰子子表达式)来解析通常为命中点指定的骰子字符串。 此处有构建块,但是当前的解析器实现无法处理单个复合骰子字符串之外的正则表达式(例如3d6–2d4有效,而3d6–2d4 + 1不起作用)。

下一步:

*确定是否需要完整的解析器(请求的功能是什么?)

*选择完整解析器的最小,最简单的实现(例如,利用RegEx,坚持基金会类型等)

而且,正如您在上一篇文章中所忆及的那样,Gerry Weinberg的三个规则( Jerry Weinberg,质量软件管理第2卷,第6章)建议我应该探索至少三个替代方案。

改进解析器,或使用Google和StackOverflow改进代码

幸运的是,我有一些假期,所以我花了一些时间来做一个更好的解析器。

首先,我进行了更多研究,以更加了解解析和正则表达式。 通过一些Google搜索,导致在StackOverflow上用不相关的语言撰写了几篇文章,我终于找到了一个刚好足以满足我的用例的解析器(不完全准确,但足够接近),因此我可以编写一个适合Dice类的版本。 我可能可以稍微压缩一下,使其成为单遍,但现在是两遍,首先是进行轻量级的令牌化,其次是从令牌组成Dice类。 dice()函数变为:

 公共功能骰子(来自字符串:String)->骰子?  { 
让代币= tokenize(字符串)
让骰子=解析(令牌)
返回骰子
}

tokenize()函数遍历字符串并生成令牌数组。 这里发生了更多的功能蠕变,忽略了空格和换行符,并在遇到未知字符时犯了错误:

  ///将骰子格式的字符串转换为令牌序列。 
///如果遇到未知字符,则为空数组
/// 回到。
内部函数令牌化(_字符串:字符串)-> [令牌] {
var令牌= [令牌]()
var numberBuffer = NumberBuffer()

用于string.unicodeScalars中的标量{
//数字消耗多个字符
如果CharacterSet.decimalDigits.contains(scalar){
numberBuffer.append(标量)
}其他{
//在解析之前刷新当前数字
//下一个字符
如果让值= numberBuffer.flush(){
tokens.append(.number(value))
}

//跳过空格和换行符
如果/*snip*/whitespacesAndNewlines.contains(scalar){
继续
}

如果让令牌=令牌(来自:标量){
tokens.append(令牌)
}其他{
//如果遇到未知字符,
//停止标记化并返回一个空数组。
print(“错误,未知字符:\(标量)”)
numberBuffer.reset()
令牌= []
打破
}
}
}

如果让值= numberBuffer.flush(){
tokens.append(.number(value))
}

返回令牌
}

tokenize()函数使用轻量级数字缓冲区结构来管理字符到Int的转换:

  ///一个内部缓冲区,用于解析字符串中的数字。 
私有结构NumberBuffer {
专用var缓冲区:String =“”

变异func append(_标量:UnicodeScalar){
buffer.append(字符(标量))
}

变异func flush()-> Int? {
后卫!buffer.isEmpty else {return nil}
推迟{buffer =“”}
返回Int(缓冲区)
}

变异func reset(){
缓冲=“”
}
}

令牌本身是一个枚举,带有少量关联数据,并有助于构造和自省。 哦,我再次使用了reduce()

  ///此解析器支持的令牌类型。 
内部枚举令牌{
案例号(整数)
案例mathOperator(String)
案件死亡
大小写(字符串)

静态让mathOperatorCharacters = CharacterSet(charactersIn:
CompoundDice.mathOperators.keys.reduce(“”,+))
静态let dieCharacters = CharacterSet(charactersIn:“ dD”)
静态let dropCharacters = CharacterSet(charactersIn:
DroppingDice.Drop.allValues.map({$ 0.rawValue})。reduce(“”,+))
静态let percentCharacters = CharacterSet(charactersIn:“%”)

init?(来自标量:UnicodeScalar){
如果Token.mathOperatorCharacters.contains(scalar){
自我= .mathOperator(String(scalar))
}否则,如果Token.dieCharacters.contains(scalar){
自我= .die
}否则,如果Token.dropCharacters.contains(scalar){
自我= .drop(String(scalar))
}否则,如果Token.percentCharacters.contains(scalar){
自我= .number(100)
}其他{
返回零
}
}

var isDropping:布尔{
// TODO:这是比较枚举的最紧凑的方法吗?
警卫队.drop(_)= self else {return false}
返回真
}
}

然后,骰子解析器将遍历令牌以组成骰子:

  ///将令牌数组转换为Dice。 
内部函数解析(_令牌:[令牌])->骰子? {
var parsedDice:骰子? =无

var状态= DiceParserState()
做{
for(index,token)in tokens.enumerated(){
切换令牌{
case .number(让值):
尝试state.parse(number:value)
案例.die:
尝试state.parseDie()
情况.drop(let drop):
尝试state.parse(drop:drop)
案例.mathOperator(让数学):
如果!isDropping(令牌,之后:索引){
parsedDice = state.combine(parsedDice)
}
尝试state.parse(math:math)
}
}

parsedDice = state.combine(parsedDice)
 尝试state.finishParsing() 
}
捕获让错误{
打印(“骰子解析错误:\(错误)”)
parsedDice = nil
}

返回parsedDice
}

这依赖于骰子解析器状态结构来跟踪最后的数字,骰子,数学运算符以及是否在解析Die类型的过程中:

  ///解析器处理令牌时的内部状态。 
私有结构DiceParserState {
var lastNumber:整数? =无
var lastDice:骰子? =无
var lastMathOperator:字符串? =无
var isParsingDie = false

///解析数字并将其存储在lastDice中
///边数或lastNumber
变异func parse(number:Int)引发{
如果isParsingDie {
守卫lastDice ==其他
{抛出DiceParseError.consecutiveDiceExpressions}
守卫让死= Die(rawValue:数字)其他
{throw DiceParseError.invalidDieSides(number)}
让时间= lastNumber ?? 1个
lastDice = SimpleDice(die,times:times)
isParsingDie =假
lastNumber =无
}其他{
守卫lastNumber ==其他
{抛出DiceParseError.consecutiveNumbers}
lastNumber =数字
}
}

///启动解析die表达式; 何时完成
///将骰子面解析为整数。
更改func parseDie()引发{
警卫!isParsingDie else
{抛出DiceParseError.consecutiveDiceExpressions}
isParsingDie = true
}

///解析DroppingDice支持的放置骰子。
///必须以SimpleDice和'-'数学运算符开头。
变异func parse(drop:String)引发{
守卫让simpleDice = lastDice为? 其他简单骰子
{抛出DiceParseError.missingSimpleDice}
守卫lastMathOperator ==“-” else
{抛出DiceParseError.missingMinus}

让diceDrop = DroppingDice.Drop(rawValue:drop)!
lastDice = DroppingDice(simpleDice,drop:diceDrop)
lastMathOperator = nil
}

///解析CompoundDice支持的数学运算符。
变异func parse(math:String)引发{
守卫lastMathOperator ==其他
{抛出DiceParseError.consecutiveMathOperators}
lastMathOperator =数学
}

//从最后一个数字(DiceModifier)返回一个Dice
//或lastDice,并重置其状态。
变异func flush()->骰子? {
让returnDice:骰子?

如果让数字= lastNumber {
returnDice = DiceModifier(数字)
lastNumber =无
} if let dice = lastDice {
returnDice =骰子
lastDice =无
}其他{
returnDice =无
}

返回returnDice
}

///从当前解析的骰子返回组合的骰子
///以lhs传入,当前解析状态为rhs。
///
///如果没有当前解析的骰子,则当前解析
///返回状态。 如果没有数学运算符或
///没有rhs,返回lhs。
变异func Combine(_ lhsDice:Dice?)-> Dice? {
守卫let lhsDice = lhsDice else {return flush()}
守卫让mathOperator = lastMathOperator,
让rhsDice = flush()else {返回lhsDice}

//如果我们有一个左手边,一个数学运算符和
//将其合并到右侧。
让returnDice = CompoundDice(lhs:lhsDice,rhs:rhsDice,
mathOperator:mathOperator)
lastMathOperator = nil

返回returnDice
}

//在解析结束时检查无效或不完整状态。
func finishParsing()引发{
如果isParsingDie {
抛出DiceParseError.missingDieSides
} if lastMathOperator!= nil {
抛出DiceParseError.missingExpression
}
}
}

让我们记住-我三篇文章中的第三次-我没有计算机科学背景。 在我看来,这不是一个特别好的解析器,我只是需要它比以前做的更好。 我认为它成功了。

值得一提的是,我多次重写了Combine()函数。 第一次使它工作时(例如,通过了单元测试点),有很多条件逻辑,并且每次我单独呆了五分钟,代码流对我来说都没有任何意义,所以我一直重写它,直到代码流讲述了某种故事并保持了黄金之路,所以下次我不得不重新审视时,不必多说。 还是有点愚蠢,但这至少对我来说是有意义的。

最后,状态结构将抛出少量枚举的错误异常,以脱离解析。 我之所以加入它,是因为我希望对某些特定类型的解析失败有一定的了解和报告。 我首先逐字列举了几个可能的错误,然后编写了将它们抛出的代码,然后编写了测试点来检查它们。 是的,这是倒退,但这是我所做的(很抱歉,习惯)。 重复上一篇有关DDT和测试的文章,我确保打破实现或测试以验证每个测试点,因为我没有正式遵循TDD

警告 。 当前, parse()函数将吃掉错误并仅打印它们。 我不想向客户暴露错误,因为我不认为这是最终的实现。 如果我有更多时间,我可能会尝试找出一种使NSRegularExpression正常工作的方法,因为我想这会节省很多代码。

无论如何,错误处理是计算机科学中的两个难题之一。 不,等等,三个! 三个难题。 稍等片刻 让我们看一下…… “命名,缓存失效,一处错误和错误处理。”是的 ,三个!

有了这个功能,我就可以重新启用失败的测试点,并添加了更多测试点,尤其是负面测试,以保持完整的代码覆盖率。

最后,呼吁采取行动!

现在 ,在GitHub RolePlayingCore存储库中查看Dice 。 除了Dice之外 ,还支持使用Foundation UnitsMeasurement来解析货币,体重和身高,并开始支持种族特质,阶级特质和从JSON读取的玩家。 我们需要完成游戏,添加设备,以及……地牢地图! 加入我们! 有助于!