Blog

Funktionales Bowling in Scala

Arjan Blokzijl

Aktualisiert Oktober 23, 2025
7 Minuten

Ich habe vor vielen Jahren in Robert Martins Buch 'Agile Softwareentwicklung' über die Implementierung des Bowlingspiels im XP-Stil gelesen. Die Episode ist auch online zu finden. Vor kurzem hat er Clojure gelernt und versucht, das Bowlingspiel in Clojure zu implementieren. Es ist eine nette Übung, und obwohl ich Clojure mag, sehe ich mich in keiner Weise in der Lage, einen solchen Versuch zu wiederholen. Abgesehen davon hat Stuart Halloway, Autor des ausgezeichneten Buches 'Programming in Clojure', dies bereits viel besser gemacht, als ich es jemals könnte. Da ich mit Scala etwas vertrauter bin, dachte ich, es wäre eine nette Übung, funktionales Bowling damit auszuprobieren. Meine Scala-Kenntnisse sind in einem beklagenswerten Zustand und befinden sich auf dem Niveau eines Anfängers, so dass ich Gefahr laufe, mich völlig zu blamieren. Aber ich werde das Risiko eingehen und zumindest versuchen, aus dieser Erfahrung zu lernen.

Lassen Sie uns zunächst noch einmal die Regeln des Bowlings erläutern:

  1. Das Spiel besteht aus 10 Frames. In jedem Frame hat der Spieler zwei Möglichkeiten, 10 Pins umzuwerfen. Die Punktzahl für den Frame ergibt sich aus der Gesamtzahl der umgeworfenen Pins, plus Boni für Strikes und Spares.
  2. Ein Spare ist, wenn der Spieler alle 10 Pins in zwei Versuchen umwirft. Der Bonus für diesen Frame ist die Anzahl der Pins, die beim nächsten Wurf umgeworfen werden.
  3. Ein Strike ist, wenn der Spieler bei seinem ersten Versuch alle 10 Pins umwirft. Der Bonus für diesen Frame entspricht dem Wert der nächsten beiden geworfenen Kugeln.
  4. Im zehnten Frame darf ein Spieler, der einen Spare oder Strike wirft, die zusätzlichen Kugeln werfen, um den Frame zu vervollständigen. Es dürfen jedoch nicht mehr als drei Kugeln im zehnten Frame geworfen werden.

Wir müssen also auf irgendeine Art und Weise den Überblick über die Frame Scores eines Spiels behalten. Der Einfachheit halber verwende ich einfach eine Folge von Ganzzahlen, die ein Spiel repräsentiert. Jede Ganzzahl steht für die Anzahl der Pins, die bei jedem Wurf umgeworfen wurden. Diese Sequenz sollte in eine Folge von Frames aufgeteilt oder umgewandelt werden, wenn Sie so wollen. Ich habe mit etwas Ähnlichem begonnen: [scala] case class Frame(first:Int, second:Int, third:Int) { override def toString() = {"firstThrow: " + first + " secondThrow" + second + " thirdThrow: " + third} } def frames(g:List[Int]):List[Frame] = { g Spiel { Fall Nil => Null Fall x::Nil => Liste(new Frame(x, 0, 0)) Fall x::xs::Nil => Liste(new Frame(x, xs, 0)) case 10 :: x::xs => new Frame(10, x, xs.head) :: frames(xs.tail) case x::xs::xss if ((x + xs) == 10) => new Frame(x, xs, xss.head) :: frames(xss.tail) Fall x::xs => new Frame(x, xs.head, 0) :: frames(xs.tail) } } [/scala] Aber das könnte ein hervorragender Kandidat für das thedailywtf sein. Das kann sicherlich nicht die Art und Weise sein, wie man richtiges Scala schreibt, und abgesehen davon ist es auch nicht sehr generisch und erweiterbar. Also, nachdem ich ein wenig darüber nachgedacht habe, ein zweiter Versuch. Zunächst einmal: Warum eine Klasse, wenn wir Funktionen haben? Eine Folge von Frames kann einfach als eine Liste von Listen ausgedrückt werden, etwa so: [scala] def frames(g:List[Int]):List[List[Int]] = { if (g.isEmpty) Nil sonst { val throws = throws_for_frame(g) g.take(throws)::frames(g.drop(throws)) } } def frames(g:List[Int]):List[List[Int]] = { if (g.isEmpty) Nil sonst g.take(throws_for_frame_score(g))::frames(g.drop(throws_in_frame(g))) } def throws_for_frame_score(rolls:List[Int]):Int = { if (strike(Rollen) || spare(Rollen)) 3 sonst 2 } def throws_inframe(rolls:List[Int]):Int = { if (strike(rolls)) 1 else 2 } def strike(rolls:List[Int]):Boolean = { rolls.headOption.getOrElse(false) == 10 } def spare(rolls:List[Int]):Boolean = { rolls.take(2).foldLeft(0)(+_) ==10 } [/scala] Gar nicht so schlecht. Wenn Sie es aus der Ferne betrachten, scheinen einige kleine Hilfsfunktionen wie Strike und Spare sogar mit einigen der oben definierten Bowlingregeln zusammenzuhängen. Ein Frame ist jetzt einfach eine Liste, die entweder aus 2 oder 3 Elementen besteht, je nachdem, ob ein Strike oder Spare erzielt wurde. Jedes Element ist eine ganze Zahl, die die umgeworfenen Pins enthält. Die Auswertung eines Spiels wird nun zu einer ziemlich trivialen Angelegenheit. Wir nehmen einfach die 10 Frames, die gebowlt wurden, und addieren die in jedem Frame erzielten Pins. [scala] def scoregame(g:List[Int]):Int = { framescores(g).foldLeft(0)(+_) } def framescores(game:List[Int]):List[Int] = { framesfor(game).take(10).map(l => l.foldLeft(0)(+_)) } def framesThrown(g:List[Int]) = { frames(g).length } [/scala] In der REPL können Sie diese Funktionen ganz einfach testen:

scala>  framesThrown(Liste(4,5))
res1: Int = 1
scala>  framesThrown(Liste(4,5,10,3,4,6,7,2))
res2: Int = 4
scala>  framescores(Liste(4,5))
res3: List[Int] = List(9)
scala>  framescores(Liste(4,5,6,3))
res4: List[Int] = List(9, 9)
scala>  framescores(Liste(5,5,6,3))
res5: Liste[Int] = Liste(16, 9)
scala>  framescores(Liste(5,5,6,3,10,10,3))
res6: List[Int] = List(16, 9, 23, 13, 3)
scala>  framescores(Liste(10,10,10,10))
res4: List[Int] = List(30, 30, 20, 10)

Und um Onkel Bob zufriedenzustellen, einige Unit-Tests: [scala] Klasse BowlingTest { def repeat[T](n: Int)(what: => T): List[T] = { wenn (n==0)List.empty sonst was::wiederholen(n-1)(was) } @Test def scoreForTwoThrows = { assertEquals(9, score(Liste(4,5))) } @Test def strikeShouldGiveTwoExtraThrowsForScore = { assertEquals(Liste(30,30,20,10), framescores(repeat(4)(10))) assertEquals(24, score(Liste(5, 3, 4, 5, 3,4))) assertEquals(32, score(Liste(10, 3, 4, 5, 3))) } @Test def spareShouldGiveOneExtraThrowsForScore = { assertEquals(24, score(Liste(5, 3, 4, 5, 3, 4))) assertEquals(30, score(Liste(5, 5, 4, 5, 3, 4))) } @Test def spareAtEndShouldGiveOneExtraThrowsForScore = { assertEquals(60, score(Liste(4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 6, 5))) assertEquals(54, score(Liste(4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 5, 5))) } @Test def strikeAtEndShouldGiveTwoExtraThrowsForScore = { assertEquals(65, score(Liste(4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 10, 5, 5))) assertEquals(54, score(Liste(4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 5, 5))) } @Test def tears = { assertEquals(299, score(Liste(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 9))) } @Test def perfectGameShouldScore300 = { assertEquals(300, score(repeat(12)(10))) } @Test def allOnesShouldScore20 = { assertEquals(20, score(repeat(20)(1))) } } [/scala] Es ist immer noch etwas vereinfacht (z.B. prüft framesThrown nicht wirklich, ob ein Frame beendet ist, d.h. ob zwei oder drei Würfe gemacht wurden, es gibt keine Validierung, dass die Anzahl der umgeworfenen Pins nicht größer als 10 sein kann, Spiele können unendlich lang sein, usw. usw.). Ich werde es jedoch vorerst dabei belassen und später versuchen, es zu perfektionieren. Bis jetzt war es auf jeden Fall schon eine nette Übung. Wie gesagt, meine Scala-Kenntnisse sind so gering, dass diese Implementierung höchstwahrscheinlich noch stark verbessert werden kann. Wenn Sie Verbesserungsvorschläge haben (oder eine eigene Implementierung haben), sind Ihre Kommentare sehr willkommen. Update Wie Ilian Berci festgestellt hat, war meine Zufriedenheit über meinen ursprünglichen Versuch völlig fehlgeleitet. Irgendwie habe ich es geschafft, die Bowlingregeln völlig falsch zu interpretieren. Ich hatte mir in den Kopf gesetzt, dass ein Strike zwei zusätzliche Würfe in jedem Frame zur Folge hätte. Wenn Sie jedoch selbst einmal mit dem Bowling begonnen haben, wissen Sie, dass dies völliger Unsinn ist. Es ist nur so, dass die nächsten beiden Würfe (die dann Teil eines weiteren Frames sind) zu dem Frame beitragen, in dem der Strike erzielt wird. Erst ab dem zehnten Frame erhält der Bowler einen zusätzlichen Frame (der aus maximal zwei Würfen besteht), wenn er bei seinen letzten Spielen einen Strike erzielt. Meine ursprüngliche Framescores-Funktion und Hilfsfunktion sahen also wie folgt aus: [scala] def frames(g:List[Int]):List[List[Int]] = { if (g.isEmpty) Nil else { val throws = throws_for_frame(g) g.take(throws)::frames(g.drop(throws)) } } def throws_for_frame(rolls:List[Int]):Int = { if (strike(rolls) || spare(rolls)) 3 else 2 } [/scala] was völlig falsch ist, da es alle Würfe, die zu einem Frame-Score beitragen, in denselben Frame setzt (und sie aus der verbleibenden Spieleliste entfernt). Das führt dazu, dass ein Spiel in meiner ursprünglichen Version bis zu 30 Würfe dauern konnte, anstatt der maximal möglichen 21 Würfe. In meiner ursprünglichen Version verhielten sich die Framescores-Funktionen also wie folgt:

scala>  framescores(Liste(5,5,6,3))
res14: Liste[Int] = Liste(16, 3)
scala>  framescores(List(10,10,10,10,10))
res15: Liste[Int] = Liste(30,20)

Die Korrektur hat jedoch nicht viel Zeit in Anspruch genommen, was eine Erleichterung war, weil ich dachte, ich hätte es komplett vermasselt. Ich habe den Beitrag mit einer Version aktualisiert, die (hoffentlich) jetzt korrekt ist (so dass Sie die Korrektur in der Frame-Funktion und den Hilfsfunktionen throws_for_frame_score und throws_in_frame sehen können), und auch die Tests in einige sinnvollere aktualisiert. Die wtf-Version ist immer noch im Originalzustand, eine Korrektur würde sie wahrscheinlich sogar noch schrecklicher machen als das Original. Vielen Dank an Ilian für den Hinweis, Onkel Bob ist eindeutig unschlagbar.

Verfasst von

Arjan Blokzijl

Contact

Let’s discuss how we can support your journey.