Auf dem .NET OpenSpace Süd in Karlsruhe hatte ich heute die Gelegenheit bei einem Coding Dojo mit Ilker Cetinkaya mitzumachen. Leider ist das Dojo nicht so richtig in Fahrt gekommen und wir haben es praktisch nicht geschafft auch nur eine einzige Regel der Kata.RomanNumerals zu implementieren. Der Grund dafür war (sicherlich nicht nur) aus meiner Sicht, dass es zwei starke Fraktionen innerhalb des Teams gab. Auf der einen Seite “Coder”, die sich tatsächlich von den Tests treiben lassen wollten und auf der anderen Seite eine Gruppe der “Knobler”, die sich nicht mit dem Test-first-Ansatz identifizieren können und lieber am Board einen Algorithmus entwickeln.
Aus meiner Sicht sind beide Ansätze für sich betrachtet auch völlig in Ordnung und können zu sehr guten Lösungen führen.
Wer z.B. nicht glaubt, dass der strikte Test-first-Ansatz für Kata.RomanNumerals funktioniert, der kann gern die Teilnehmer vom 2.ten Hamburger Coding Dojo fragen oder das github-Repository dazu durch browsen und die Entwicklung der Commits verfolgen.
Was aber offensichtlich überhaupt nicht funktioniert ist der Mittelweg. Wenn sich beide Gruppen nicht auf das Vorgehen einigen und im Zweifel immer nur der “lauteste” Änderungsvorschlag gewinnt, dann wird das Ergebnis mitunter unbefriedigend für beide Seiten. Beim Karlsruher-Dojo hatten wir am Ende nach einer gescheiterten Implementierungsphase und abrupten Abbruch sogar nur 7 rote und einen grünen Test vorzuweisen. Bei dem Grünen frage ich momentan sogar immer noch warum ausgerechnet dieser Test überhaupt gelaufen ist.
Um also eine solche Situation zu vermeiden sollte man sich also am Anfang entscheiden wie man sein Dojo gemeinsam durchziehen will. Im weiteren werde ich jetzt eher auf die von mir favorisierte Variante mit striktem TDD eingehen, auch wenn mich eine Knobler-Session mit ausgeprägter Analysephase auch mal als Vergleichswert interessieren würde.
Wie gesagt: Ich bin ausdrücklich davon überzeugt, dass beide Lösungswege funktionieren können – je nach Problem mal besser und mal schlechter.
Striktes TDD und der einfachste Lösungsvorschlag gewinnt
Ein oft genanntes Argument der “Knobler”-Fraktion am TDD-Ansatz ist, dass nicht vorrausschauend entwickelt wird. Hey Jungs – das ist doch genau der Trick. Solange ich keine Anforderung für irgendwelche Dinge habe (nicht im Kopf oder auf dem Papier, sondern als Test) wird keine Komplexität hinzugefügt. Es wird dann behauptet, dass bei Test-first am Anfang sinnfreie Implementierungen gewählt werden und dass man diesen Code am Ende ganz offensichtlich sowieso wieder wegschmeißen würde. Aber das stimmt einfach nicht. Auch wenn ich immer mehr Tests hinzunehme und meine Implementierung immer generischer wird, so wird die vorherige Lösung letztlich immer ein Spezialfall der generischen Variante sein und geht somit nie verloren.
Gerade der Fokus auf diese minimal mögliche Generalsierung der aktuellen Implementierung sorgt dafür, dass der Algorithmus am Ende korrekt herausfällt. Das Knobeln kommt an dieser Stelle natürlich wieder in den Ablauf herein. Allerdings knobeln wir so an wesentlich kleineren und damit leichteren Problemen – selbstverständlich nun viel öfter und in kürzeren Zyklen.
Bei zukünftigen Coding Dojos würde ich also gern als Regel sehen, dass der einfachste Vorschlag, der zu Grün führt implementiert wird.
Temposteigerung durch gruppenweises Anlegen von roten Tests
Zu bestimmten Zeitpunkten und insbesondere auch am Anfang von vielen Katas sieht man jedoch oft Implementierungsmuster die gleich eine ganze Klasse an Tests abdecken könnten. Wenn man so eine Idee hat, dann sollte man die natürlich auch nutzen und so das Tempo steigern. Anstatt aber diese Idee einfach zu programmieren würde ich erst die komplette Gruppe von Tests schreiben und sicherstellen, dass alle neuen Tests auch wirklich "rot sind. Dieser Shortcut ist aus meiner Sicht methodisch völlig unproblematisch, da zu diesem Zeitpunkt nur neue Anforderungen im System dokumentiert werden und diese dann als Begründung für neue Komplexität herangezogen werden können.
Aber auch hier sollte die selbe Regel gelten: Die leichteste Implementierung gewinnt. Wenn also ein Teilnehmer eine einfachere Lösung anbieten kann, dann wird die genommen.
Training durch Wiederholung
Wie Ilker in der Auswertung bereits gesagt hat, würde ich auch gern nochmal in der selben Truppe ein Dojo machen. Vielleicht ergibt sich das ja sogar beim nächsten OpenSpace. Denn selbstverständlich geht es beim Coding Dojo nicht nur um Code sondern auch um die beteiligten Menschen.
Obwohl wir am Ende keine Lösung vorweisen konnten, habe ich in der kurzen Zeit sehr viel über die Entwicklungsmethodik anderer Teilnehmer gelernt – ich denke das hat mir wirklich eine Menge gebracht.
Training durch Wiederholung – Reloaded
Da ein wichtiges Element bei Katas auch die Wiederholung ist (und mir im Zug langweilig ist), habe ich zusätzlich für mich selbst und andere noch eine aufgeräumtere Variante der Kata mit minimalen Commits in das Repository hochgeladen. Unter http://github.com/forki/DojoHamburg/commits/Karlsruhe kann man die Änderungen von unten nach oben genau nachvollziehen. Wenn jemand ein Stelle findet wo ich zu viel implementiert habe oder wo die Änderung nicht sinnvoll und einfach erscheint, dann würde ich mich freuen darüber diskutieren zu können.
Tags:
.Net Open Space Süd,
CodingDojo
Gestern haben wir bei der msu solutions GmbH am Standort Halle unser erstes internes CodingDojo veranstaltet.
Ziele des internen Dojos
Da wir in der tagtäglichen Arbeit mit Microsoft Dynamics NAV entwickeln, wollten wir das Dojo auch mit Dynamics NAV und der Programmiersprache C/AL abhalten. Als wichtiges Ziel der Veranstaltung haben wir uns vorgenommen nach Test-Driven Development (TDD) zu arbeiten. Da TDD im NAV-Bereich (sicherlich mangelns Tools und Community) noch so gut wie unbekannt und unser selbstentwickeltes Test-Framework noch sehr jung ist, war diese Selbstverpflichtung für einige Team-Mitglieder in dieser strikten Form sicherlich noch Neuland.
Ablauf
Um die Messlatte für das erste Dojo daher nicht allzu hoch zu legen und mehr Fokus auf den Versionsverwaltung (git), die Test-Tools und die Kommunikation bzw. den Prozess selbst zu richten habe ich die relativ einfachen Katas FizzBuzz und DictionaryReplacer vorgeschlagen. Das Team hat sich dann für FizzBuzz entschieden.
Wie beim 1. NET CodingDojo in Hamburg gab es auch in Halle einige Startschwierigkeiten. Es scheint für viele Entwickler aufgrund der jahrelangen Entwicklungspraxis sehr schwer zu sein, tatsächlich zwischen Test und Implementierung zu unterscheiden.
Nachdem jedoch die ersten 3 Tests samt Implementierung geschafft und die explizite Trennung der Test- und Entwicklungsphasen erkannt wurden steigerte sich das Tempo des Teams enorm. So konnten wir dann auch Erweiterungen der Aufgabenstellung diskutieren und umsetzen.
Ich denke unser erstes msu Dojo war ein Erfolg und insbesondere die Wahl der Kata war für die erste Veranstaltung dieser Art ausgezeichnet. Für das nächste Mal wollen wir uns dann natürlich an eine schwierigere Kata wagen.
Randori-Stil
Da seit dem ”Dojo Deluxe” von und mit Ralf Westphal eine Diskussion über die Dojo-Formate aufgekommen ist, möchte ich hier auch noch meine Meinung dazu abgeben.
Da wir uns den Randori-Stil ausgesucht haben, gab es eigentlich vier verschiedene Rollen. Als erstes natürlich den Driver und den Co-Piloten, also eine Person, die die Tastatur bedient und eine die sagt “wo es lang geht”. Übrig bleiben dann in unserer Version noch der Rest des Teams als Publikum und ein Moderator. Da es die erste Veranstaltung dieser Art war, gab es noch sehr viele Hinweise vom Moderator. Bei nachfolgenden Veranstaltungen würde ich mir wünschen, dass der Moderator immer mehr in den Hintergrund rückt und die jeweiligen Co-Piloten deutlich aktiver werden. Das Publikum könnte noch mehr als das Kontrollorgan fungieren und auf Einhaltung der selbst auferlegten Richtlinien pochen. Als wichtigen Punkt für nachfolgende Veranstaltungen würde ich also noch eine klarere Trennung der Rollen sehen und dem Team mehr Eigenverantwortung auferlegen.
Randori-Stil vs. Tutorial
Die Befähigung des Teams den Entwicklungsprozess ohne starke führende Hand selbst zu kontrollieren ist für mich ein Kernziel bei einem CodingDojo. Ich denke hier unterscheidet sich der Randori-Stil (jedenfalls wie ich ihn bisher verstehe) stark von der Variante und den Zielstellungen die Ralf Westphal in seinem Dojo in München abgehalten hat. Wie Ilker Cetinkaya auch schon geschrieben hat ist Ralf’s Dojo-Variante wahrscheinlich näher am klassischen Workshop oder am Tutorial einzuordnen. Daran ist natürlich nichts falsch, ganz im Gegenteil: ich finde solche Workshops mit erfahrenen und charismatischen Sprechern extrem reizvoll. Insbesondere weil man bei einem Workshop aufgrund der intensiveren Vorbereitung des Sprechers in der kürzeren Zeit eine weit höhere fachliche Tiefe erreicht und man im Gegensatz zu klassischen Frontal-Vorträgen auch mehr Rückfragen stellen kann. Ralf hat sich dazu in seinem Blog ja auch intensiv und fundiert Gedanken gemacht.
“Einer weiß etwas und die anderen wollen es von ihm lernen”-Formate wie Ralf sie beschreibt sind also auch aus meiner Sicht sehr nützlich und bilden vermutlich sogar die Voraussetzung für ein Randori. Wenn nicht genug Leute im Raum bereits etwas über die zu benutzenden Techniken oder Methoden gehört haben, dann wird das Dojo bzw. das gemeinsame Lernen und Lehren vermutlich sehr zäh und der Moderator wird zum Hauptakteur. Es ist somit auch völlig einleuchtend, dass Ralf seine Event-Based Components natürlich nicht in einem Randori vorstellen kann. Das wäre dann wohl beim einem leeren FlipChart geblieben und ganz sicher auch eine Enttäuschung für alle Seiten.
Wenn jedoch hinreichend viele Leute bereits Erfahrungen mit der Materie gemacht haben, dann gibt es vermutlich auch unterschiedliche Interpretationen. Diese würde ich aber nicht so drastisch wie Ralf als “Unsicherheit” auslegen, sondern eher als Chance die Varianten gemeinsam zu diskutieren und Unklarheiten auszuräumen. Dabei muss es am Ende aber nicht zwingend einen Konsens geben.
Daher denke ich, dass auch ein Format mit weit weniger Moderation bzw. Anleitung wichtig ist, insbesondere auch um das bisher Erlernte in einer sicheren Umgebung zu testen und echtes TeamPlay zu trainieren. Den Begriff “Dojo” ausschließlich für solche freien Formate zu verwenden, ist wie Ralf beschrieben hat aus historischer Sicht wahrscheinlich eher ungeschickt. Dem stimme ich zu.
Ich denke hier hat Ralf hat meine Twitter-Kommentare evtl. auch etwas überbewertet (140 Zeichen sind natürlich oft auch missverständlich). Ich bin auch nicht dafür, dass alle Entwickler gleichgemacht werden sollen und es überhaupt keinen Lehrer gibt. Ganz im Gegenteil jeder soll sich einbringen, denn selbstverständlich sind immer unterschiedliche Wissengrade im Raum zu finden.
Dass alle etwas beisteuern klappt dann aber nur wenn die erfahreneren Entwickler sich auch mal darauf einlassen einen vorgeschlagenen “Irrweg” mit zu gehen und den Erkenntnisprozess so reifen zu lassen. Abgabe von Kontrolle ist zugegebenermaßen nicht gerade einfach, aber manchmal wird man vielleicht auch als alter Hase noch überrascht werden.
Tags:
CodingDojo,
Dynamics NAV 2009,
msu-solutions-GmbH
Gestern fand das erste .NET Coding Dojo in Hamburg statt und es war aus meiner Sicht ein voller Erfolg. Als erstes möchte ich mich an dieser Stelle bei der Firma masters of arts für die Räumlichkeit und natürlich bei allen Teilnehmer für die aktive Mitarbeit bedanken.
Durchgeführt haben wir das Dojo im Randori-Stil, d.h. es gab einen Entwickler, einen “Co-Piloten” und das Publikum. Nach jeweils 5 Minuten bzw. jedem neuen Testfall wurden die Positionen mit neuen Teilnehmer aus dem Publikum getauscht.
Nach einer kurzen allgemeinen Einführung in das Thema “Coding Dojo” habe ich die Aufgabenstellungen der Kata.FizzBuzz und Kata.DictionaryReplacer vorgestellt. Durch kurze Abstimmung haben wir dann (vermutlich durch meine Wortwahl beeinflusst) die Kata.DictionaryReplacer gewählt.
Mit der Wahl dieser Kata war ich nicht ganz unglücklich, da wir DictionaryReplacer schon im Online Dojo durchgenommen haben und ich deshalb die Fallstricke schon etwas einschätzen konnte.
Kata.DictionaryReplacer
Create a method that takes a string and a dictionary,
and replaces every key in the dictionary pre and suffixed with a dollar sign,
with the corresponding value from the Dictionary.
Tests
input : "",
dict empty
output: ""
input : "$temp$",
dict ["temp", "temporary"]
output: "temporary"
input : "$temp$ here comes the name $name$",
dict ["temp", "temporary"] ["name", "John Doe"]
output : "temporary here comes the name John Doe"
[codingdojo.org]
Nach kurzer Einführung in die Vorgehensweise des Test-Driven Developments (TDD) und Diskussion über sinnvolles Naming ging es dann auch richtig in die Entwicklung. Die Kata eignet sich in dieser Form auch sehr schön um tatsächlich “Test first” zu arbeiten. Die Implementierung folgt quasi als logische Konsequenz.
Interessant und ungleich komplizierter wurde es dann als wir eine zusätzliche Anforderung eingebaut haben. Da es sich bei der Kata um eine Art Templatesystem handelt liegt es nahe auch in den Values des Wörterbuchs neue Keys zu erlauben und somit eine beliebige Ersetzungstiefe zu erreichen. Diese Anforderung ist ein wunderbares Beispiel dafür wie sehr man mit Aufwandsabschätzungen daneben liegen kann. 😉
Die rekursive Ersetzung wurde noch sehr schnell gelöst, aber richtig schwer haben wir uns dann mit der Erkennung von zyklischen Ersetzungen und der daraus folgenden Endlosschleife getan. Nach und nach wurden immer kompliziertere Lösungsmöglichkeiten genannt, die aber in der gegebenen Zeit nicht fehlerfrei umgesetzt werden konnten. Als Folge haben wir immer noch einen Test “rot”. Wer Lust hat kann sich also die Quellen vom github Repository ziehen und als Hausaufgabe den verbleibenden Test “grün” machen.
In der Nachbetrachtung haben wir noch einige Sachen festgestellt:
Was lief gut?
- Die Rotation der Teilnehmer klappte problemlos
- Lösungsideen sprudelten förmlich aus dem Publikum
- Interessante Edge-Cases wurden gefunden
- Viele kleine Tipps (z.B. Resharper Shortcuts) wurden ausgetauscht
- TDD wurde als erfolgreich für die Kata angesehen
- Die Randori-Form sorgt für Aktivität
- Ganz wichtig: Es hat Spaß gemacht!
Was wollen wir besser machen?
- Schneller den ersten Test schreiben
- Intensivere Diskussion des Interfaces
- Noch mehr Wert auf das Naming legen
- Versuchen die einfachste Lösung zu wählen
- Mehr Wert auf Refactoring legen
- wurde etwas vernachlässigt, da wir viel Zeit an der Endlosschleife verbraucht haben
- Die Rollen Driver, Co-Pilot und Publikum klarer trennen
- Nach jedem implementierten Testcase bzw. jeder Rotation der Teilnehmer in das git Repository einchecken
- Von Anfang an externe Tastatur und Maus für den Laptop bereit stellen
- Internetverbindung um Syntaxfrage schneller klären zu können
Als möglichen Termin für das Dojo haben wir jetzt grob jeweils den 3. Mittwoch im Monat vorgeschlagen. Über die Xing-Gruppe der .NET User Group Hamburg werden wir natürlich auch wieder entsprechende Einladungen verschicken.
Tags:
.NET User Group Hamburg,
CodingDojo,
TDD,
Test Driven Design
Today I’m starting a new blog post series about solving code katas in F# and with the help of my NaturalSpec project. A code kata is a programming exercise which helps to improve your skills through practice and repetition. In this series we want to use the Test Driven Development TDD approach which means in the context of NaturalSpec that we have to write our specs before we implement the algorithm.
Problem Description
“The game of yahtzee is a simple dice game. Each round, each player rolls five six sided dice. The player may choose to reroll some or all of the dice up to three times (including the original roll). The player then places the roll at a category, such as ones, twos, sixes, pair, two pairs etc. If the roll is compatible with the score, the player gets a score for this roll according to the rules. If the roll is not compatible, the player gets a score of zero for this roll.
The kata consists of creating the rules to score a roll in any of a predefined category. Given a roll and a category, the final solution should output the score for this roll placed in this category.”
[codingdojo.org]
Category 1 – Ones, Twos, Threes, Fours, Fives, Sixes
“Ones, Twos, Threes, Fours, Fives, Sixes: The player scores the sum of the dice that reads one, two, three, four, five or six, respectively. For example, 1, 1, 2, 4, 4 placed on "fours" gives 8 points.”
After reading this category description we could come up with the following spec:
module Yahtzee.Specs
open NaturalSpec
let placed_on category list =
printMethod category
calcValue category list
[<Scenario>]
let “Given 1,1,2,4,4 placed on "fours" gives 8 points.“ () =
Given (1, 1, 2, 4, 4)
|> When (placed_on Fours)
|> It should equal 8
|> Verify
Since we really want to implement six different categories we should also add some more scenarios like this one:
[<Scenario>]
let “Given 1,1,6,4,6 placed on "sixes" gives 12 points.“ ()=
Given (1, 1, 6, 4, 6)
|> When (placed_on Sixes)
|> It should equal 12
|> Verify
// …
Now we have some specs but they will all fail since we don’t have anything implemented yet. So we have to come up with a model of dice rolls and categories. As the specs suggests we model the dice roll as a tuple of ints and the category as a discriminated union:
module Yahtzee.Model
type Roll = int * int * int * int * int
type Category =
| Ones
| Twos
| Threes
| Fours
| Fives
| Sixes
The tuple is a natural choice for the dice roll, but for easier calculation we add a helper function which converts it into a list. This allows use to use the standard list functions and therefore summing the values becomes trivial:
let toList (roll:Roll) =
let a,b,c,d,e = roll
[a;b;c;d;e]
let sumNumber number =
Seq.filter ((=) number)
>> Seq.sum
let calcValue category roll =
let list = toList roll
match category with
| Ones -> sumNumber 1 list
| Twos -> sumNumber 2 list
| Threes -> sumNumber 3 list
| Fours -> sumNumber 4 list
| Fives -> sumNumber 5 list
| Sixes -> sumNumber 6 list
Now we can run our scenarios with any NUnit runner. I’m using the default NUnit GUI runner here, which gives me a picture like this:
Category 2 – Pair
“Pair: The player scores the sum of the two highest matching dice. For example, 3, 3, 3, 4, 4 placed on "pair" gives 8.”
The kata description gives us some new scenarios. As seen above we should specify them before writing the code.
[<Scenario>]
let “Given 3,3,3,4,4 placed on "pair" gives 8.“ () =
Given (3, 3, 3, 4, 4)
|> When (placed_on Pair)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 5,3,5,4,4 placed on "pair" gives 10.“ () =
Given (5, 3, 5, 4, 4)
|> When (placed_on Pair)
|> It should equal 10
|> Verify
[<Scenario>]
let “Given 1,2,3,4,5 placed on "pair" gives 0.“ () =
Given (1, 2, 3, 4, 5)
|> When (placed_on Pair)
|> It should equal 0
|> Verify
Since we use a new category we now have to extend our model and the calcValue function:
type Category =
| Ones
// …
| Sixes
| Pair
// …
let sumAsPair list number =
let numberCount =
list
|> Seq.filter ((=) number)
|> Seq.length
if numberCount >= 2 then 2 * number else 0
let calcValue category roll =
let list = toList roll
match category with
| Ones -> sumNumber 1 list
// …
| Sixes -> sumNumber 6 list
| Pair ->
[1..6]
|> Seq.map (sumAsPair list)
|> Seq.max
Category 3 – Two pairs
“Two pairs: If there are two pairs of dice with the same number, the player scores the sum of these dice. If not, the player scores 0. For example, 1, 1, 2, 3, 3 placed on "two pairs" gives 8.”
[<Scenario>]
let “Given 1,1,2,3,3 placed on "two pair" gives 8.“ () =
Given (1, 1, 2, 3, 3)
|> When (placed_on TwoPair)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 1,6,6,3,3 placed on "two pair" gives 18.“ () =
Given (1, 6, 6, 3, 3)
|> When (placed_on TwoPair)
|> It should equal 18
|> Verify
[<Scenario>]
let “Given 1,1,2,4,3 placed on "two pair" gives 0.“ () =
Given (1, 1, 2, 4, 3)
|> When (placed_on TwoPair)
|> It should equal 0
|> Verify
Implementing this category is a little bit tricky but with the help of our Pair function and some more standard sequence combinators we can get our spec green:
type Category =
| Ones
// …
| TwoPair
let allPairs =
[for i in 1..6 do
for j in 1..6 -> i,j]
let calcValue category roll =
// …
| TwoPair ->
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsPair list a
let b’ = sumAsPair list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
Category 4 – Three of a kind
“Three of a kind: If there are three dice with the same number, the player scores the sum of these dice. Otherwise, the player scores 0. For example, 3, 3, 3, 4, 5 places on "three of a kind" gives 9.”
[<Scenario>]
let “Given 3,3,3,4,5 placed on "three of a kind" gives 9“()=
Given (3, 3, 3, 4, 5)
|> When (placed_on ThreeOfAKind)
|> It should equal 9
|> Verify
[<Scenario>]
let “Given 3,4,3,4,5 placed on "three of a kind" gives 0“()=
Given (3, 4, 3, 4, 5)
|> When (placed_on ThreeOfAKind)
|> It should equal 0
|> Verify
Now it is time to refactor our code. The sumAsPair function should be extended to a sumAsTuple function:
type Category =
| Ones
// …
| ThreeOfAKind
let sumAsTuple value list number =
let numberCount =
list
|> Seq.filter ((=) number)
|> Seq.length
let takeBestTuple value list =
[1..6]
|> Seq.map (sumAsTuple value list)
|> Seq.max
if numberCount >= value then value * number else 0
let calcValue category roll =
// …
| Pair -> takeBestTuple 2 list
| TwoPair ->
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsTuple 2 list a
let b’ = sumAsTuple 2 list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
| ThreeOfAKind –> takeBestTuple 3 list
Category 5 – Four of a kind
“Four of a kind: If there are four dice with the same number, the player scores the sum of these dice. Otherwise, the player scores 0. For example, 2, 2, 2, 2, 5 places on "four of a kind" gives 8.”
[<Scenario>]
let “Given 2,2,2,2,5 placed on "four of a kind" gives 8“ ()=
Given (2, 2, 2, 2, 5)
|> When (placed_on FourOfAKind)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 2,6,2,2,5 placed on "four of a kind" gives 0“ ()=
Given (2, 6, 2, 2, 5)
|> When (placed_on FourOfAKind)
|> It should equal 0
|> Verify
With the help of the takeBestTuple function this becomes trivial:
type Category =
| Ones
// …
| FourOfAKind
let calcValue category roll =
// …
| FourOfAKind -> takeBestTuple 4 list
Category 6 – Small straight
“Small straight: If the dice read 1,2,3,4,5, the player scores 15 (the sum of all the dice), otherwise 0.”
[<Scenario>]
let “Given 1,2,3,4,5 placed on "Small Straight" gives 15“()=
Given (1,2,3,4,5)
|> When (placed_on SmallStraight)
|> It should equal 15
|> Verify
[<Scenario>]
let “Given 1,2,5,4,3 placed on "Small Straight" gives 15“()=
Given (1,2,5,4,3)
|> When (placed_on SmallStraight)
|> It should equal 15
|> Verify
[<Scenario>]
let “Given 1,2,6,4,3 placed on "Small Straight" gives 0“()=
Given (1,2,6,4,3)
|> When (placed_on SmallStraight)
|> It should equal 0
|> Verify
As in all the above scenarios we don’t assume any specific order in our rolls but for this category it is easier to test if the data is sorted:
type Category =
| Ones
// …
| SmallStraight
let calcValue category roll =
// …
| SmallStraight ->
match list |> List.sort with
| [1;2;3;4;5] -> 15
| _ -> 0
Category 7 – Large straight
“Large straight: If the dice read 2,3,4,5,6, the player scores 20 (the sum of all the dice), otherwise 0.”
[<Scenario>]
let “Given 2,3,4,5,6 placed on "Large Straight" gives 20“()=
Given (2,3,4,5,6)
|> When (placed_on LargeStraight)
|> It should equal 20
|> Verify
[<Scenario>]
let “Given 6,2,5,4,3 placed on "Large Straight" gives 20“()=
Given (6,2,5,4,3)
|> When (placed_on LargeStraight)
|> It should equal 20
|> Verify
[<Scenario>]
let “Given 1,2,6,4,3 placed on "Large Straight" gives 0“()=
Given (1,2,6,4,3)
|> When (placed_on LargeStraight)
|> It should equal 0
|> Verify
Of course the implementation is exactly the same as for the small straight:
type Category =
| Ones
// …
| LargeStraight
let calcValue category roll =
// …
| LargeStraight ->
match list |> List.sort with
| [2;3;4;5;6] -> 20
| _ -> 0
Category 8 – Full house
“Full house: If the dice are two of a kind and three of a kind, the player scores the sum of all the dice. For example, 1,1,2,2,2 placed on "full house" gives 8. 4,4,4,4,4 is not "full house".”
[<Scenario>]
let “Given 1,1,2,2,2 placed on "full house" gives 8.“ () =
Given (1,1,2,2,2)
|> When (placed_on FullHouse)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 4,4,4,4,4 placed on "full house" gives 0.“ () =
Given (4,4,4,4,4)
|> When (placed_on FullHouse)
|> It should equal 0
|> Verify
[<Scenario>]
let “Given 1,1,2,3,2 placed on "full house" gives 0.“ () =
Given (1,1,2,3,2)
|> When (placed_on FullHouse)
|> It should equal 0
|> Verify
Implementing the FullHouse category is easy if we reuse our solutions to the Two pairs category:
type Category =
| Ones
// …
| FullHouse
let takeBestCombo value1 value2 list =
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsTuple value1 list a
let b’ = sumAsTuple value2 list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
let calcValue category roll =
// …
| TwoPair -> takeBestCombo 2 2 list
// …
| FullHouse -> takeBestCombo 2 3 list
Category 9 – Yahtzee
“Yahtzee: If all dice are the have the same number, the player scores 50 points, otherwise 0.”
Here we can use NaturalSpec’s ScenarioTemplates in order to specify all Yahtzees:
[<ScenarioTemplate(1)>]
[<ScenarioTemplate(2)>]
[<ScenarioTemplate(3)>]
[<ScenarioTemplate(4)>]
[<ScenarioTemplate(5)>]
[<ScenarioTemplate(6)>]
let “Given n,n,n,n,n placed on "Yahtzee" gives 50.“ n =
Given (n,n,n,n,n)
|> When (placed_on Yahtzee)
|> It should equal 50
|> Verify
[<Scenario>]
let “Given 1,1,1,2,1 placed on "Yahtzee" gives 50.“ () =
Given (1,1,1,2,1)
|> When (placed_on Yahtzee)
|> It should equal 0
|> Verify
The implementation is pretty easy:
type Category =
| Ones
// …
| Yahtzee
let calcValue category roll =
// …
| Yahtzee ->
let a,b,c,d,e = roll
if a = b && a = c && a = d && a = e then 50 else 0
Category 10 – Chance
“Chance: The player gets the sum of all dice, no matter what they read.”
[<Scenario>]
let “Given 1,1,1,2,1 placed on "Chance" gives 6.“ () =
Given (1,1,1,2,1)
|> When (placed_on Chance)
|> It should equal 6
|> Verify
[<Scenario>]
let “Given 1,6,1,2,1 placed on "Chance" gives 11.“ () =
Given (1,6,1,2,1)
|> When (placed_on Chance)
|> It should equal 11
|> Verify
This seems to be the easiest category as we only have to sum the values:
type Category =
| Ones
// …
| Chance
let calcValue category roll =
// …
| Chance -> List.sum list
Conclusion
We used a lot of F#’s sequence combinators, pattern matching and discriminated unions in this kata. I think this shows that F# is very well suited for such a problem and with NaturalSpec we can easily use a TDD/BDD approach.
The complete source code can be found in the NaturalSpec repository.
If you want to know more about a specific part of the kata or NaturalSpec feel free to contact me.
Tags:
BDD,
Code Quality,
F#,
Kata,
KataYahtzee,
NaturalSpec,
nunit,
TDD