So, Dice is in GitHub now (3/3)

Brian Arnold
11 min readJun 10, 2017

--

I’ve noticed it’s still 2017, and I’ve still been working on getting my sea-legs with GitHub. I started with an article about a Dice class, and I followed up with an article about how I tested Dice and added a parser. This is the third and final article, getting all the Dice things into GitHub.

Approaching the castle of the ranger-knight…

Deciding on a license

I researched an appropriate software license for Dice, or more generally RolePlayingCore, and chose the MIT license, because it provides a great deal of openness and flexibility, while generally avoiding being bat-shit crazy. In my time as a developer, I’ve dealt with more than several handfuls of libraries of open and closed source licenses. I’ve also dealt with the legal departments and counsels at every company I’ve worked, and I’ve seen my share of lawsuits and breach-of-contract. I’ve learned there are some kinds of licenses and terms that are worth working with, and some that aren’t. Generally speaking, if a license is longer than one page, bad things could happen, down the road. And they often will.

This license is worth working with, for this context, so I chose it for RolePlayingCore.

Continuously integrating the CI script

Once I had the code and an initial README.md committed, I moved on to multiple branches and continuous integration. Well, two branches anyway, master and development, for now.

As for continuous integration, I chose Travis CI because it supports iOS, and it was at the top of the list of choices. Unfortunately, I floundered a bit. I can be a real idiot sometimes. After turning it on, I was in one of those hurry-up, “it should just work” modes, and it… just… didn’t, over and over again.

My .travis.yml file had 12 failing submitted edits between the first submission that didn’t actually build anything, to the submission that actually built successfully. I sometimes fall into this mode, where I keep beating my head on a problem senselessly, until something finally gives way. The elapsed time was three days, mostly because I have a day job; it only actually wasted between one and two hours of my life. In this case, having no idea what “success” looks like, I just kept guessing, sometimes wildly, until I finally decided to check other GitHub iOS repositories for help. My difficulties weren’t actually that bad, mainly: (1) get the Xcode version right, (2) get the scheme name right, (3) get the simulator name right. I was just that stupid about getting them right.

Here is what it was when it actually started working:

language: objective-cosx_image: xcode8.3script:- set -o pipefail- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "RolePlayingCore" -destination "platform=iOS Simulator,name=iPhone 7" build-for-testing test | xcpretty

It’s a start. But not yet done. Done would include device testing, and more.

One thing I might consider doing is set up a local Travis so I can qualify changes before pushing them out.

One thing I don’t know how to do yet is ensure that code coverage remains at 100%, and fail if it falls below that, or some threshold. Readers, do you know how to set this up?

Managing the context

Any time software development reaches integration, third party dependencies become significant to review, and manage. In this case, there are no other libraries, but there is the compiler I’m using to build with, and the simulator I’m using to test with. Both gave me trouble.

One of the things that failed in my Adventures in specifying a .travis.yml file, was an existing bug in a Travis osx_image, specifically Xcode 8.2 simulator devices — they didn’t work because there were duplicates installed — so I made sure to add that Travis issue to my watchlist. And, sure enough, it got addressed with the Travis Xcode 8.3 image, which is one of the reasons I moved to Xcode 8.3. The other reason I moved to Xcode 8.3 is because Apple fixed a bug in Swift.

I know, right? Apple fixing bugs? It had to do with the mathOperators map. If I had more than 3, the compiler would die with “expression-too-complex.” I really wanted all the operators, well, the usual four, plus “x” as a variant of “*” for multiply, so I’m glad Apple fixed something in the compiler that enabled me to have them. Note to self: self, always file a Radar.

Making code sharing happen

I’m only going so far as making the repository available as an iOS framework at this time. I’m not ready yet to deal with CocoaPods, et al. I am seriously considering contributing to Swift Packages to help move it along to a point where it supports frameworks, but I don’t have the bandwidth right now to entertain that. I’d also like to develop an example client app or three, so I have a better sense of what makes sense to make public (in the Swift exporting sense), before I go there.

My first GitHub “Issue”

One of the first issues I wanted to raise using the GitHub Issues feature was the initial dice parser. Being that it sucked, I had taken the easy way out, and it’s what I could produce in a very short amount of time. I described the issue thusly in GitHub:

The current requirement for DiceParser is to parse dice strings that are typically specified for hit points, with rolls, modifiers, dropping a throw, and compound dice (multiple dice sub-expressions). The building blocks are there, but the current parser implementation can’t handle regular expressions beyond a single compound dice string (e.g., 3d6–2d4 works, but 3d6–2d4+1 does not).

Next steps:

* Decide if a full parser is a requirement (what is the requested feature?)

* Choose the smallest, simplest implementation of a full parser (e.g., leverage RegEx, stick to Foundation types, etc.)

And, as you’ll recall from the previous article, Gerry Weinberg’s rule of three (Jerry Weinberg, Quality Software Management Vol 2, Chapter 6) suggests that I should explore at least three alternatives.

Improving the parser, or better code with Google and StackOverflow

As luck would have it, I had some vacation time, and so I spent some of it making a better parser.

First, I did some more research to get a little bit more familiar with parsing and regular expressions. Through some Google searching that led to a couple of posts on StackOverflow in unrelated languages, I finally found a just-enough parser that matches my use case (not exactly, but close enough) so I could write a version suited for the Dice classes. I probably could condense it a little more to make it single-pass, but for now it’s two-pass, first to do lightweight tokenization, and second to compose the Dice classes from the tokens. The dice() function became this:

public func dice(from string: String) -> Dice? {
let tokens = tokenize(string)
let dice = parse(tokens)
return dice
}

The tokenize() function walks the string and produces an array of tokens. A little bit more feature creep happened here, ignoring whitespaces and newlines, and erring out if an unknown character is encountered:

/// Converts a dice-formatted string into a sequence of tokens.
/// If an unknown character is encountered, an empty array is
/// returned.
internal func tokenize(_ string: String) -> [Token] {
var tokens = [Token]()
var numberBuffer = NumberBuffer()

for scalar in string.unicodeScalars {
// Numbers consume multiple characters
if CharacterSet.decimalDigits.contains(scalar) {
numberBuffer.append(scalar)
} else {
// Flush the current number before parsing
// the next character
if let value = numberBuffer.flush() {
tokens.append(.number(value))
}

// Skip spaces and newlines
if /*snip*/whitespacesAndNewlines.contains(scalar) {
continue
}

if let token = Token(from: scalar) {
tokens.append(token)
} else {
// If an unknown character is encountered,
// stop tokenizing and return an empty array.
print("Error, unknown character: \(scalar)")
numberBuffer.reset()
tokens = []
break
}
}
}

if let value = numberBuffer.flush() {
tokens.append(.number(value))
}

return tokens
}

The tokenize() function uses a lightweight number buffer struct for managing the conversion of characters into an Int:

/// An internal buffer for parsing numbers from a string.
private struct NumberBuffer {
private var buffer: String = ""

mutating func append(_ scalar: UnicodeScalar) {
buffer.append(Character(scalar))
}

mutating func flush() -> Int? {
guard !buffer.isEmpty else { return nil }
defer { buffer = "" }
return Int(buffer)
}

mutating func reset() {
buffer = ""
}
}

Tokens themselves are an enumeration, with a little bit of associated data and help with construction and introspection. Ooh, and I used reduce() again:

/// Types of tokens supported by this parser.
internal enum Token {
case number(Int)
case mathOperator(String)
case die
case drop(String)

static let mathOperatorCharacters = CharacterSet(charactersIn:
CompoundDice.mathOperators.keys.reduce("", +))
static let dieCharacters = CharacterSet(charactersIn: "dD")
static let dropCharacters = CharacterSet(charactersIn:
DroppingDice.Drop.allValues.map({ $0.rawValue }).reduce("", +))
static let percentCharacters = CharacterSet(charactersIn: "%")

init?(from scalar: UnicodeScalar) {
if Token.mathOperatorCharacters.contains(scalar) {
self = .mathOperator(String(scalar))
} else if Token.dieCharacters.contains(scalar) {
self = .die
} else if Token.dropCharacters.contains(scalar) {
self = .drop(String(scalar))
} else if Token.percentCharacters.contains(scalar) {
self = .number(100)
} else {
return nil
}
}

var isDropping: Bool {
// TODO: is this the most compact way to compare an enum?
guard case .drop(_) = self else { return false }
return true
}
}

The dice parser would then iterate over the tokens to compose the Dice:

/// Converts an array of tokens into Dice.
internal func parse(_ tokens: [Token]) -> Dice? {
var parsedDice: Dice? = nil

var state = DiceParserState()
do {
for (index, token) in tokens.enumerated() {
switch token {
case .number(let value):
try state.parse(number: value)
case .die:
try state.parseDie()
case .drop(let drop):
try state.parse(drop: drop)
case .mathOperator(let math):
if !isDropping(tokens, after: index) {
parsedDice = state.combine(parsedDice)
}
try state.parse(math: math)
}
}

parsedDice = state.combine(parsedDice)
try state.finishParsing()
}
catch let error {
print("Dice parse error: \(error)")
parsedDice = nil
}

return parsedDice
}

This relied on a dice parser state struct to keep track of the last number, dice, math operator and whether in the middle of parsing a Die type:

/// The internal state of the parser when it processes tokens.
private struct DiceParserState {
var lastNumber: Int? = nil
var lastDice: Dice? = nil
var lastMathOperator: String? = nil
var isParsingDie = false

/// Parses a number and stores it either in lastDice
/// sides or lastNumber
mutating func parse(number: Int) throws {
if isParsingDie {
guard lastDice == nil else
{ throw DiceParseError.consecutiveDiceExpressions }
guard let die = Die(rawValue: number) else
{ throw DiceParseError.invalidDieSides(number) }
let times = lastNumber ?? 1
lastDice = SimpleDice(die, times: times)
isParsingDie = false
lastNumber = nil
} else {
guard lastNumber == nil else
{ throw DiceParseError.consecutiveNumbers }
lastNumber = number
}
}

/// Initiates parsing a die expression; finishes when
/// parsing dice sides as an integer.
mutating func parseDie() throws {
guard !isParsingDie else
{ throw DiceParseError.consecutiveDiceExpressions }
isParsingDie = true
}

/// Parses a dropping dice supported by DroppingDice.
/// Must be preceded by a SimpleDice and a '-' math operator.
mutating func parse(drop: String) throws {
guard let simpleDice = lastDice as? SimpleDice else
{ throw DiceParseError.missingSimpleDice }
guard lastMathOperator == "-" else
{ throw DiceParseError.missingMinus }

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

/// Parses a math operator supported by CompoundDice.
mutating func parse(math: String) throws {
guard lastMathOperator == nil else
{ throw DiceParseError.consecutiveMathOperators }
lastMathOperator = math
}

// Returns a Dice from either the last number (DiceModifier)
// or lastDice, and resets their state.
mutating func flush() -> Dice? {
let returnDice: Dice?

if let number = lastNumber {
returnDice = DiceModifier(number)
lastNumber = nil
} else if let dice = lastDice {
returnDice = dice
lastDice = nil
} else {
returnDice = nil
}

return returnDice
}

/// Returns combined dice from the current parsed dice
/// passed in as lhs, and the current parse state as rhs.
///
/// If there is no current parsed dice, the current parse
/// state is returned. If there is no math operator or
/// no rhs, lhs is returned.
mutating func combine(_ lhsDice: Dice?) -> Dice? {
guard let lhsDice = lhsDice else { return flush() }
guard let mathOperator = lastMathOperator,
let rhsDice = flush() else { return lhsDice }

// If we have a left hand side, a math operator and
// a right hand side, combine them.
let returnDice = CompoundDice(lhs: lhsDice, rhs: rhsDice,
mathOperator: mathOperator)
lastMathOperator = nil

return returnDice
}

// Checks for invalid or incomplete state at the end of parsing.
func finishParsing() throws {
if isParsingDie {
throw DiceParseError.missingDieSides
} else if lastMathOperator != nil {
throw DiceParseError.missingExpression
}
}
}

Let’s remember — for the third time in three articles — that I do not have a background in computer science. In my opinion, this is not an especially good parser, I just needed it to be better than what I had done before. And I think it succeeded at that.

It’s worth mentioning that I rewrote the combine() function several times. The first time I got it working (as in, the unit test points passed), there was a lot of conditional logic, and the code flow didn’t make any sense to me every time I left it alone for five minutes, so I kept rewriting it until the code flow tells some kind of story and maintains the golden path, so there’s less to have to reason about, next time I have to revisit. It’s still a little goofy, but it makes sense to me, at least.

Finally, the state struct would throw a small number of enumerated error exceptions to break out of parsing. I included this because I wanted to have some visibility and reporting of specific types of parsing failures. I literally enumerated several possible errors first, then I wrote the code to throw them, and then the test points to check for them. Yes, this is backwards, but it’s what I did (sorry again, habit). Repeating the lesson in my previous article on DDT and testing, I made sure to break either the implementation or the test to validate each test point, since I don’t formally follow TDD.

Caveat. Currently, the parse() function eats the errors and just prints them. I don’t want to expose errors yet to clients, because I don’t think this is the final implementation. If I had a little bit more time, I might try to figure out a way to get NSRegularExpression to work, as I imagine it would save me a lot of code.

Anyway, error handling is one of the two hard problems in computer science. No, wait, three! Three hard problems. Hold a sec. Let’s see… “naming things, cache invalidation, off-by-one errors, and error handling.” Yep, three!

With this in hand, I was able to re-enable the failing test points, and I added several more test points, notably negative tests, to maintain full code coverage.

Finally, a call to action!

Gah! A Stone Giant!

Right now, go check out Dice in the GitHub RolePlayingCore repository. In addition to Dice, there is support for parsing currency, weight and height using Foundation Units and Measurement, and a start for racial traits, class traits and players read from JSON. We need to finish players, and add equipment, and… dungeon maps! Join us! Contribute!

--

--

Brian Arnold

Director, Mobile at wellframe.com. Formerly at mathworks.com, apple.com, lumina.com. I do Swift & iOS. Debugging Guru. Jedi Padawan. Lego Fan. Wine Buff.