Freitag, 13. April 2007

Dieser Blog ist tot. Ich blogge weiter auf dem «Agile Trail».

Conway's Game of Life - Quiz-Lösung in Groovy

Ich habe bei Matt Secoskes GroovyQuiz mitgemacht und eine Lösung für Conways Spiel des Lebens in Groovy erstellt. Dabei bin ich testgetrieben vorgegangen und hab' viel Spaß mit Groovy gehabt.

Zuerst habe ich eine Version erstellt, die ein Universum Tick für Tick auf der Konsole ausgibt:

  XXX
XXX

X
X X
X X
X

Danach habe ich eine Oberfläche mit dem SwingBuilder erstellt:



Ich habe ein Jar per Gant erzeugt, mit welchem das Progrämmchen ausgeführt werden kann. Da ich selbst immer wieder an den Einstellschrauben des Programms rumgespielt habe, dachte ich mir, dass das vielleicht auch für den Benutzer nett sein könnte. Wenn man also nun das Groovy-Programm per
java -jar conwaysgameoflife-64.jar -h

aufruft, dann gibt's das hier:
Usage: java -jar conwaysgame.jar [arguments]
-b <blockSize> sets the length of a block in
pixel;
default value is 10
-h prints this help text
-p <pattern> sets the inital pattern (try
TOAD, BLINKER or GUN);
default value is [' ',' XXX ',' XXX ',' ']
-t <tickInterval> sets the tick interval in
milliseconds;
default value is 200
-v <view> sets the view to <swing>
or <console>;
default value is swing


Die Blockgröße, der Tickintervall und die Ausgabeart sollten klar sein. Das Muster, mit dem das Programm startet, ist dagegen schon interessanter: Ich habe drei typische Muster (Toad, Blinker und Gun) quasi vorinstalliert, und man kann deren Namen angeben.
java -jar conwaysgameoflife-64.jar
-p BLINKER

Zusätzlich dazu kann man aber auch alternativ ein eigenes Muster angeben, etwa
java -jar conwaysgameoflife-64.jar
-p "['XX ','XX ',' XX',' XX']"

Die Syntax dazu sollte allen bekannt vorkommen, die schonmal mit Groovy rumgespielt haben :-)

Lessons learned habe ich auch eine ganze Menge:
  • Gant ist nett. Ich habe ein Buildskript für dieses Progrämmchen geschrieben und es in Eclipse per External Tools... eingebunden. Liest sich IMHO viel besser als antiges XML.
  • Closures isoliert zu testen ist nicht trivial. Man schaue sich hierzu an, welche Verrenkungen ich machen mußte, um den Ruler zu testen (siehe RulerTest.groovy). Ich bin da erstmal glücklich gewesen, überhaupt das machen zu können, was ich wollte - aber da sind noch nicht die letzten Zeilen Code und Blogwörter drüber geschrieben worden!
  • Der SwingBuilder ist hakelig, zumindest wenn man auf Graphics arbeiten möchte. Vielleicht mag da Danno Ferrin bald einiges schöner groovyfizieren. Im Nachhinein habe ich mich ja geärgert: Warum nicht gleich mit Grails machen...?!
  • Matt schlug vor, eine DSL für die Rules zu machen. Dafür war der von mir verwendete Builder fast schon überflüssig, denn die Rules sehen in Standard-Groovy-Notation schon fast genug DSLig aus:
    new Rule(from:Cell.LIVE,
    withLivingNeighbours:0..1,
    to:Cell.DEAD
    new Rule(from:Cell.LIVE,
    withLivingNeighbours:3..9,
    to:Cell.DEAD)
    new Rule(from:Cell.LIVE,
    withLivingNeighbours:2..3,
    to:Cell.LIVE)
    new Rule(from:Cell.DEAD,
    withLivingNeighbours:3,
    to:Cell.LIVE)

    Mit eigenem Builder ist's nur minimal besser lesbar:
    new RuleBuilder().rules {
    rule(from:Cell.LIVE,
    withLivingNeighbours:0..1,
    to:Cell.DEAD)
    rule(from:Cell.LIVE,
    withLivingNeighbours:3..9,
    to:Cell.DEAD)
    rule(from:Cell.LIVE,
    withLivingNeighbours:2..3,
    to:Cell.LIVE)
    rule(from:Cell.DEAD,
    withLivingNeighbours:3,
    to:Cell.LIVE)
    }
  • Ausführbare Jars mit integrierten Libs in Java zu erzeugen ist ein Tritt in den Hintern für jeden, der's versucht. One Jar ist da eine mögliche Antwort drauf. Ich habe es mir einfacher gemacht und das komplette groovy-all-1.0.jar per zipfileset in Gant in mein eigenes Jar gedrückt. Fein ist was anderes :-(
  • Lustig war dann wieder das Programmieren mit Methodenzeigern (ich haßte sie in C++!):
    def view = new GroovyShell(
    new Binding(Main:Main.class))
    .evaluate("Main.&${arguments.view}")
    view(universe, arguments)

    Es gibt zwei Methoden in der Main-Klasse, die die Ausgabeart darstellen: swing und console, und diese werden mit den Übergabeparametern -v swing oder -v console so wie oben direkt angesprochen.
  • Ähnlich einfach kann ich per GroovyShell das Muster der Startuniversen parsen, wenn ich das Programm aufrufe mit -p ['XX','XX']:
    static calculatePattern(arguments) {
    if(arguments.pattern[0] != '[')
    arguments.pattern =
    Main."${arguments.pattern}"
    return new GroovyShell(
    new Binding(Main:Main.class))
    .evaluate(arguments.pattern)
    }

    Wenn ich im Pattern-Argument kein sich öffnende eckige Klammer an erster Stelle entdecke, dann verweise ich direkt auf die entsprechenden Konstanten, also z.B. BLINKER, die so aussehen:
    static def BLINKER = "['   ','XXX','   ']"
    Und solch einen String kann man dann mit der GroovyShell auswerten lassen. Das ist Groovy! :-)

    Update 13.04.2007 21:30 h: GroovyShell ist groovy und einfach, aber noch grovier und noch einfacher geht's trotzdem noch. Die GroovyShell benutzt man eher aus Javaklassen heraus, um Groovy-Code auszuführen. Es gibt eine Klasse groovy.util.Eval, die die GroovyShell innerhalb von Groovyklassen bequemer zugreifbar macht. Statt
    return new GroovyShell(
    new Binding(Main:Main.class))
    .evaluate(arguments.pattern)
    schreibe ich da einfach
    return Eval.me(arguments.pattern)
    und fertig ist die Einfachheit. Okay, ist ein wenig geflunkert, denn das Binding habe ich nicht übernommen, denn es war hier tatsächlich überflüssig gewesen. Nicht so bei
    def view =
    new GroovyShell(
    new Binding(Main:Main.class))
    .evaluate("Main.&${arguments.view}")
    . Um hier das Binding aufrecht zu erhalten, schreibe ich nun
    def view =
    Eval.me('Main',
    Main.class,
    "Main.&${arguments.view}")
    . Und es geht noch einfacher: Eval bietet drei Methoden, x(), xy() und xyz(), die zwei bis vier Parameter erwarten, von denen das letzte der zu evaluierende Ausdruck sein muss und die übrigen direkt ins Binding gegeben werden und dann als x, y oder z verfügbar sind. Die view-Methode bekomme ich dann also mit
    def view =
    Eval.x(Main.class,
    "x.&${arguments.view}")
    . Ich liebe solch kleine Optimierungsschmankerl!

Hier gibt's das ausführbare Jar sowie die kompletten Sourcen samt Buildskript, Libs und Eclipse-Dateien. Wer nur mal die Sourcen überfliegen möchte, für den habe ich byteMyCode strapaziert und einfach alle Klassen nacheinander da reingekippt.

Danke an Matt Secoske für diese kurzweilige Programmieraufgabe, an Glen Pepicelli und Peter Ledbrook für Ihre Hilfen beim Problem mit ausführbaren Jars, an Danno Ferrin und Guillaume Laforge für Ihre Hilfen bei meinen Malversuchen im SwingBuilder und an Stefan Roock fürs Zuhören bei meinem Rumgeheule übers Closure-Testen.

blog comments powered by Disqus