所以,我上了骰子课(3之1)

现在是2017年。而且,我一直在努力让GitHub发挥作用。

我添加的第一个类是Dice类,它属于我创建的存储库,称为RolePlayingCore。 关于目标或一般要求,我想代表骰子掷骰 ,例如2d4 + 1, 已在GitHub中集成和测试 。 当我弯腰朝着这个目标前进时,我跳了一些更具体的要求。

老实说,我不确定这个新存储库的去向。 我可能不知道有一天我想开发一个角色扮演游戏。 好吧,好吧,我想有一天开发一个角色扮演游戏。 实际上,我想从过去半年来在Swift中工作的D&D风格的地图数据开始,但是它仍然太不成熟,以至于我认为这将是一个糟糕的开始。

但是,骰子。 我想我可以绕开骰子。 所以,我从那开始。

哦。 顺便说一句,本文是三篇中的第一篇。 这一篇涵盖了我想要实现的东西的基本设计。 下一个涵盖测试和解析。 然后,第三个将所有内容捆绑到GitHub。

“年轻的程序员最好不要从CS的理论基础入手,而要急于编写糟糕的程序。”-Paul Graham,@ paulg

关于沙盒驱动开发(SDD)

我认为我用于开发代码的过程可能称为MDD或“由人为驱动的开发”。或者,更为积极的自旋可能是SDD或“沙盒驱动的开发”。我没有在计算机上受过正式教育科学(我学习过机械工程),并且是1980年代的后遗症,所以我的研究方法是街头聪明人和守旧派。 但是我可以和做TDD的好孩子挂在一起,除了在编写测试时,我的方法更像DDT或“ Development-Driven-Testing”。在这种情况下,我喜欢在代码中探究不同的潜在设计。 -最近在Swift Playgrounds中-直到我对需求是什么,它们的含义以及如何在代码中满足它们有了更好的了解。 这并不是说测试根本不会发生。 哦,发生了。 在操场上,可以检查中间结果,并且可以在代码中穿插非正式测试,这些代码将在每次运行时执行。 这是临时的,但松散的耦合有助于进行设计试验。 通过反复试验,我开始弄清需求,编写单元测试并在首选设计上进行迭代。

在这个骰子课程的发展中,我肯定会弯腰。 在操场上。

混凝土基础

我的实现倾向于(相对于说来总是)以具体的东西(对不起,习惯)开始。 然后,我慢慢地朝着类似于设计的方向努力。 因此,基本的具体Dice类(在这种情况下为struct)封装了基本意图。 说到习惯,我试图养成先使用结构而不是类进行设计的习惯,直到确定设计需要类为止。 在这种情况下,对于像“两卷d4加1”这样的场景,我可以按照以下方式实现Dice

  ///骰子侧面,掷骰子的次数可选, 
///和一个修饰符。
公共结构骰子{
公众让步:诚信
公开租借时间:
public let修饰符:Int

///创建骰子卷。 (可选)指定滚动时间,
///和一个修饰符。 默认为滚动一次,然后
/// 0的修饰符。
public init(sides:Int,times:Int = 1,修饰符:Int = 0){
self.sides =边数
self.times =次
self.modifier =修饰符
}

///滚动指定的次数,返回总和
///卷。
public func roll()-> Int {
var rolls = 0
对于_ in 0 .. <次{
卷+ = Int(arc4random_uniform(UInt32(sides))+1)
}
回滚+修饰符
}
}

边栏 :我倾向于使用三斜杠(///),以便在帮助中显示注释。 在更高级的代码中,我也可以利用markdown。 我也倾向于将public用于可能暴露给客户的事情。 您将在下一篇文章中看到原因。

在混凝土上戳孔

同时…我很快就决定, 太通用 (原始int,相关int的数量非常有限),而roll() 太具体 (硬编码的随机数实现,对于初学者来说是“加1”) ) 因此,我用Die枚举代​​替了 ,该枚举代表了从d4到d%的通常嫌疑犯,一个roll()函数用于一次掷骰子,以及一个随机部分的抽象:

  ///模头尺寸的枚举,从d4到d%。 
公共枚举Die:Int {
情况d4 = 4
情况d6 = 6
情况d8 = 8
情况d10 = 10
情况d12 = 12
情况d20 = 20
情况d100 = 100 //也就是“ d%”
  ///滚动一次并返回1到此模具类型之间的数字。 
public func roll()-> Int {
返回random(self.rawValue)
}
}

因此,实现为random 。 嗯

读者困惑:实现共享随机数生成器功能的最佳方法是什么? 如果要有一百万个Die实例,我不想将实现传递给每个实例。 我采用了通常的策略,即使用默认实现的静态随机数生成器函数,该函数可以在运行时替换,并由实例使用。 让我知道您是否会这样做,或者您是否有与此不同的想法。

从生成器开始,以协议和默认实现的形式:

 公共协议RandomNumberGenerator { 

///返回0 .. <upperBound之间的随机数。
func random(_ upperBound:Int)->整数

}
 公共结构DefaultRandomNumberGenerator:RandomNumberGenerator { 

///返回0 .. <upperBound之间的随机数。
公共函数随机(_ upperBound:Int)-> Int {
返回Int(arc4random_uniform(UInt32(upperBound)))
}
}

接下来,我在Die类中添加了静态函数,并从roll()中调用了random()函数,其中“加1”表示:

 公共静态var randomNumberGenerator:RandomNumberGenerator = 
DefaultRandomNumberGenerator()
  ///使用随机数生成器从中返回整数 
/// 1 ... upperBound。
私人函数随机(_ upperBound:Int)-> Int {
返回Die.randomNumberGenerator.random(upperBound)+ 1
}

在这一点上,Dice类具有一个Die而不是sides ,以及一个稍微抽象的roll()函数:

  ///一个简单的骰子有一个骰子,可以选择次数 
///滚动和一个修饰符。
公共结构骰子{

公众让死:死亡
公开租借时间:
public let修饰符:Int

///创建骰子卷。 (可选)指定滚动时间,
///和一个修饰符。 默认为滚动一次,然后
/// 0的修饰符。
public init(_ die:Die,times:Int = 1,修饰语:Int = 0){
self.die =死亡
self.times =次
self.modifier =修饰符
}

///滚动指定的次数,返回总和
///卷。
public func roll()-> Int {
var rolls = 0
对于_ in 0 .. <times {
卷+ = die.roll()
}
回滚+修饰符
}
}

处理需求

我对事情的进展感到满意,我拿到了最新版本的《地牢大师指南》(如果需要的话,这是一个“规范”),它使我想起了一些概念,例如丢下骰子,组合成不同骰子的骰子和多个修饰符。 哦,还有数学运算符(减号与加号,duh不同)。

拉屎。

到目前为止,我对需求的思考太模糊,太局限; 我的执行力很差。 我需要代表“ 4d6-L”(四次d6,下降最低),“ d6 + d4”等滚动。 为了放下一卷,我需要一个roll()函数来跟踪中间的卷,以及某种知道是否放下最高或最低卷的方法。 而且,为了加总不同面的骰子,我需要某种形式的构图。 而且,数学运算符。

拉屎。 拉屎。 拉屎。

将所有这些功能打包到一个类(erm,struct)中不再有意义。 设计,抬起丑陋的头。 是时候声明Dice的协议,声明以前的Dice结构为Dice的实现者,并抽象化新功能了。 稍等片刻 骰子实现骰子?

我在命名事物方面的第一个挑战

对于命名事物,我有些坚持和积极。 当我发现一个名字不太正确时,我常常在重命名之前先重命名。 例如,我迅速采取行动,将Dice用作协议名称,因为我希望客户端将Dice作为一种抽象引用,而不会被多余的单词所困扰,而不是DiceProtocol或诸如此类。 但是,以前称为Dice的结构又如何呢? 我使用SimpleDice只是为了消除歧义。 我想到了BaseDice等,但是我认为它不会被子类化,它只是一个简单的骰子类。 有时候一个名字就足够好了。 只要它不会误导或误导,它就不必是完美的。

进行刚好足够的设计(JED)

当我开始研究丢弃一卷的类(结构)时,我决定Die应该为多个卷提供功能,而Dice可以对这些卷进行自省(一个int数组)。 从协议开始:

  ///不同面的一个或多个骰子的表示 
///和组合。
公共协议骰子{

///掷骰子,并返回结果。
func roll()->整数

///返回上一掷骰子的中间结果。
///如果未进行任何滚动,则返回一个空数组。
var lastRoll:[Int] {get}
  } 

还有Die的一点帮助:

  ///滚动指定的次数并返回一个数组 
///介于1和此骰子类型之间的数字。
公共功能卷(_次:Int)-> [Int] {
//预分配比追加要快得多
var rolls = [Int](重复:0,计数:次)

对于0 .. <次{
rolls [index] = roll()
}
返回卷
}

来自一代过早的优化器,我在没有足够证据证明它很慢的情况下优化了roll() 。 我确实忘记了我偶然发现预分配的方式,我可能正在使用数组追加来进行其他非常相似的事情。 我可能观察到在操场上的性能确实很慢(自我注意: 自我,这是一个坏主意;编译后的代码比操场上的代码运行快得多,并且运行得快得多 ),并且发现预分配的速度明显要快得多,例如10的秒与1秒的秒数。 无论如何,在for循环中使用已知长度的数组时,预分配而不是使用append是我的新习惯。 即使在这种情况下,我也不知道这是否真的必要。

好。 现在,SimpleDice将失去其修饰符(因为稍后需要使用合成和数学运算符重新实现它),使用新的Die roll()函数并缓存最后一个滚动:

  /// ... 
///跟踪每次调用roll()时的最后一卷(整数数组)。
公共类SimpleDice:骰子{

公众让死:死亡
公开租借时间:
public private(set)var lastRoll:[Int] = []

///为指定的模具创建一个SimpleDice。 可选地
///指定滚动时间。 默认为滚动一次。
public init(_ die:Die,times:Int = 1){
self.die =死亡
self.times =次
}

///滚动指定的次数,返回总和
///卷。
///中间卷可以在lastRoll中进行检查。
public func roll()-> Int {
lastRoll = die.roll(次)
返回lastRoll.reduce(0,+)
}
}

我将其从结构更改为类,因为它的roll()函数需要进行突变,并且我不希望协议或从该协议派生的其他结构声称在不发生变化时也进行了突变。 可能是la脚的原因。 欢迎就此提出建议。

哦。 而且,我在其roll()函数中使用了reduce() 。 我实际上在白板上写了“ map”,“ sort”和“ reduce”,以提醒我可以使用它们使代码更紧凑更高效 。 我怀疑这些可能不会总是使代码更具可维护性 。 因此,有时我会考虑使用它们,而有时我会避免使用它们。 时间(一些实际的分析,并反思一些经过精细老化的代码)会证明一切。 给读者的问题 :您如何看待这些功能?

现在,DroppingDice:

  ///掉落的骰子是SimpleDice的扩展,它可以掉落 
///最高或最低滚动。 这是通过完成
///组成,而不是子类化。
公共结构DroppingDice:Dice {

公共让骰子:SimpleDice

///降低最低或最高滚动的选项。
公共枚举删除:字符串{
最低的情况=“ L”
最高案例=“ H”
}

///放弃最低或最高的掷骰。
公开放手:放手
  ///返回所放卷的值。 如果返回nil 
///没有卷。
public var dropRollRoll:Int? {
返回下降==。最低?
dice.lastRoll.min():dice.lastRoll.max()
}

///返回放置的卷的索引。 如果返回nil
///没有卷。
public var dropIndex:Int? {
守卫let roll = droppedRoll else {return nil}
返回dice.lastRoll.index(of:roll)
}

///返回骰子的最后一掷,并放下
///卷已删除。 要获得最后一卷而没有
///删除掉落的卷,使用dice.lastRoll。
public var lastRoll:[Int] {
守卫dice.lastRoll.count> 0 else {return []}

//我们将返回骰子的lastRoll的修改后的副本
var roll = dice.lastRoll
roll.remove(at:dropIndex!)
回滚
}

///为指定的骰子,掷骰时间创建SimpleDice,
///以及是否放弃最高或最低结果。
public init(_ die:Die,times:Int,droping:Dropping){
self.dice = SimpleDice(死,次:次)
self.dropping =丢弃
}

///滚动指定的次数,返回总和
///卷,减去掉落的卷。 中级
///可以检查卷,包括掉落的卷
/// dice.lastRoll。
public func roll()-> Int {
让_ = dice.roll()
返回self.lastRoll.reduce(0,+)
}
}

因为有一堆中间函数,它们的状态取决于是否调用过roll() ,所以这些函数中有一些保护语句,它们返回空值或nil。 我对此不满意,但是经验使我相信这没什么大不了的。 在此类(erm,struct)中,lastRoll具有一个getter,该getter始终是通过删除其骰子属性的lastRoll的最高或最低值来计算的,因此roll()函数中的奇数改组。

现在,DiceModifier:

  ///骰子修饰符是可以代替的常量 
///一个Dice实例。
公共结构DiceModifier:Dice {

public let修饰符:Int

公共初始化(_修饰符:整数){self.modifier =修饰符}

公共函数roll()-> Int {return}

公共变量lastRoll:[Int] {返回[修饰符]}
}

并且,CompoundDice:

  ///骰子卷的通用组成。 
///此类型的两个主要用例是:
///-组合两卷,例如“ 2d4 + d6”,
///-使用修饰符,例如“ d12 + 2”。
公共结构CompoundDice:Dice {

公众让我:骰子
公众让:骰子
public let mathOperator:字符串

///创建一个符合语法的骰子
///“ d ”。
///除die以外的所有参数都是可选的;
///时间默认为1,修饰符默认为0,
///,数学运算符默认为“ +”。
public init(_ die:Die,times:Int = 1,修饰语:Int = 0,
mathOperator:字符串=“ +”){
让骰子= SimpleDice(die,times:times)
let修饰符= DiceModifier(修饰符)
self.init(lhs:骰子,rhs:修饰符,
mathOperator:mathOperator)
}

///用数学运算符从两个骰子实例创建一个骰子。
public init(lhs:Dice,rhs:Dice,mathOperator:String){
self.lhs = lhs
self.rhs = rhs
self.mathOperator = mathOperator
}
  ///数学运算符或函数的函数签名。 
内部类型别名MathOperator =(Int,Int)-> Int

///字符串到函数签名的映射。
内部静态让mathOperators:[String:MathOperator] =
[“ +”:(+),“-”:(-),“ x”:(*),“ *”:(*),“ /”:(/)]
  ///滚动指定的次数,可以选择添加 
///或乘以修饰符,
///并返回结果。
public func roll()-> Int {
让lhsResult = lhs.roll()
让rhsResult = rhs.roll()
返回CompoundDice.mathOperators [mathOperator]!(lhsResult,
rhsResult)
}

///返回左手和右手的串联
///最后一卷。
公共变量lastRoll:[Int] {
保护lhs.lastRoll.count> 0 && rhs.lastRoll.count> 0 else
{return []}
返回lhs.lastRoll + rhs.lastRoll
}
}

我在这里做了一些有趣的事情,因为我将数学运算符作为字符串传入,然后将它们映射到数学函数,例如+,-等。因为我想出了一种使用数学运算符作为函数的方法呼叫。 我尝试将函数用作参数,但是发生了各种麻烦。 我不太确定自己是否做对了。 玩弄它肯定会使旧版本Xcode的编译器崩溃。

在操场上进行即席测试

记住,我说我在设计方案时在操场上进行临时测试是合理的。 为此,我还制作了CustomStringConvertible的所有内容,为简洁起见,我将它们保留在上述代码段之内。 我还向Dice协议添加了一个var lastRollDescription:字符串获取器,以便所有内容都可以字符串形式描述“将滚动的内容”以及“最后滚动的内容”。

回到我的第一个示例2d4 + 1,它最初由原始Dice完全封装,现在将通过CompoundDice的特定构造函数表示,该构造函数将SimpleDice和DiceModifier组成为默认的“ +”数学运算符:

 让骰子= CompoundDice(.d4,次:2,修饰符:1) 

这是其他示例的一些游乐场输出:

一点反思

现在,我感觉自己的设计很稳定。 我几乎从不使用UML,但是我认为尝试描述到目前为止的图表可能很有趣,因为它相对简单,但是我还没有找到一个简单易用的工具。 我尝试了OmniGraffle,但是在我为该练习分配的短时间内太麻烦了。 我用一个免费的网站genmymodel.com制作了这个简化图:

对于该设计,我仍然不太满意,例如lastRoll的处理方式,但是它足以满足我认为对Dice的要求(JED)

是时候编写测试,并使用此处设计的构建块将字符串解析为Dice表达式的时间了。 我们将在下一篇文章中介绍这些主题。