Seminararbeit Ruby Uno Kartenspiel Autor: Fabian Merki Fabian Merki 05.11.2006 1 von 10
Inhaltsverzeichnis Einleitung... 3 Die Idee... 4 Design und Implementierung in Ruby... 5 Testing... 7 Startbefehle... 8 Client / Server... 8 Tests... 8 Zusammenfassung... 9 Tools und Frameworks... 10 Fabian Merki 05.11.2006 2 von 10
Einleitung Ruby hat mir von der ersten Stunde an gefallen, da man mit wenig Code schnell sehr viel erreichen kann. Diese Arbeit soll zeigen, dass Ruby sehr gut für rundenbasierte, verteilte Spiele eingesetzt werden kann, aber auch was für Schwierigkeiten auftreten können. Und nun wünsche ich Ihnen viel Spass beim Lesen. Fabian Merki Fabian Merki 05.11.2006 3 von 10
Die Idee Da ich selbst sehr gerne Uno spiele und ich schon lange die Idee hatte, eine Laufzeit für rundenbasierte Spiele zu erstellen, in welcher sich diverse KI-Spieler gegenseitig messen können, habe ich folgendes Projekt in Angriff genommen. Anforderungen an ein in Ruby implementiertes Uno Spiel: KI-Spieler (eigenständige Applikation) registriert sich bei einem Server. KI-Spieler soll vom Server aus aufgerufen werden (Callback). Server soll die Regelnprüfen und dafür sorgen, dass niemand mogeln kann. Karten sollen objektorientiert implementiert werden. Regeln sollen möglichst nur einmal implementiert werden. Des weitern soll ein von Menschen gesteuerter Spieler programmiert werden, welcher auch mitspielen kann. Damit lässt sich prüfen, wie gut die KI-Spieler wirklich sind. Fabian Merki 05.11.2006 4 von 10
Design und Implementierung in Ruby Folgende Klassendiagramme sollen die Struktur des Programms visualisieren: Zentraler Punkt auf der Serverseite ist die Klasse Spiel und auf der Clientseite der Spieler. Damit der Spieler nicht einfach seine Karten auf dem Server löschen kann und somit gewonnen hätte, habe ich das Spieler Objekt auf dem Server als ServerSpieler kopiert und delegiere alle Aufrufe auf das effektive Spieler Objekt. Somit hat der Client keinen Zugriff auf das interne Spieler Objekt und deshalb keinen Zugriff auf seine Handkarten. Der Spieler könnte aber die Karte auf dem Stapel oder seine Karten ändern, sofern Attribute mit attr_accessor deklariert werden. Nur die Klasse FarbWahlKarte hat ein solches Attribut. Damit der Client mit seiner Mogelaktion nicht erfolgreich ist, merkt sich der Server die gewählte Farbe und prüft auf Veränderung. Alternativ könnte man immer eine Kopie der Karte auf dem Stapel an den Client übergeben. Der Server ruft den Spieler über die Methode karte(stapelkarte, direkt) auf. Der Parameter direkt zeigt an, ob eine PlusZweiKarte auf dem Stapel liegt und man das +2 direkt erhöhen darf, falls man eine PlusZweiKarte besitzt. Dank Vererbung können einfach diverse Spieler implementiert werden. Der AdvancedSpieler soll hier nur als Beispiel dienen. Dieser könnte natürlich Statistiken führen und sehr bewusst spielen. Der menschliche Spieler wird über die Konsole gesteuert. Die konkrete Fabian Merki 05.11.2006 5 von 10
Implementierung befragt einem (welche Karte gespielt werden soll) nur, wenn es mehrere Möglichkeiten gibt. Damit Callback-Methoden des Spielers und SpielInfo auf der Clientseite ablaufen, musste ich die Klasse Spieler mit include DRbUndumped definieren. Somit ist das Spieler Objekt ein verteiltes Objekt, welches vom Client dem Server übergeben wird, aber die Methoden werden dann jedoch auf dem Client ausgeführt. Auf Modelebene sind natürlich die Karten des Uno Spiels das Wichtigste. Folgendes Klassendiagramm zeigt die Klassenhierarchie der Karten. Die Methode ok(stapel) prüft, ob die Karte auf die Karte auf dem Stapel gelegt werden darf oder nicht. Die == Operation musste ich überschreiben, damit ich die Karten in Listen wieder finde und diese entfernen kann. Anscheinend wurde das Objekt beim Transfer Server -> Client -> Server kopiert. Für den Vergleich habe ich mich für ein Attribut kartenid in der Klasse Karte entschieden, welches eindeutig ist. Weitere Details entnehmen Sie bitte direkt dem Sourcecode. Fabian Merki 05.11.2006 6 von 10
Testing Für das Testen meines Uno Spiels habe ich eine Testdatei (unittest.rb) geschrieben, welche das Kartenmodel testet. Dies hat sich als sehr hilfreich erwiesen, da Ruby Programme nur zur Laufzeit auf formelle und logische Fehler geprüft werden können. Für das Spiel und die Spieler hätte man gleich verfahren können; ich habe diese aber von Hand getestet. Der Nachteil dieser Methode ist, dass man relativ häufig testen muss und diese Tests nicht unter einer Sekunde ablaufen. Es scheint, als würde sich der Unittest Aufwand in Ruby wirklich lohnen, da man hier keinen Compiler hat, der einem die formellen Fehler anzeigt. Die KI-Spieler konnte ich relativ einfach testen, indem ich einfach 5 Spieler x Mal gegeneinander antreten lies. Leider habe ich wie wohl jeder Ruby-Anfänger häufig mit puts (und nicht mit log4ruby) Debuginformationen geschrieben. Nun wollte ich aber wissen, welche KI-Spieler wie oft gewinnen, und habe folgenden kleinen Ruby-Trick angewandt: $sp={} # Merke alte puts Methode alias :orig_puts :puts # Zähle wer wie oft gewinnt (kein Output) def puts(text) if text =~ /gewinnt/ i = $sp[text] if (i == nil) i = 0 end i = i + 1 $sp[text]=i end end # 100 Spiele spielen # Statistik ausgeben $sp.each_pair { k, v orig_puts "#{k} #{v}" } Lässt man das Programm laufen, so erhält man zum Beispiel folgenden Output: ai spieler 1 gewinnt 15 ai spieler 2 gewinnt 15 advanced ai spieler 2 gewinnt 30 advanced ai spieler 1 gewinnt 24 ai spieler 0 gewinnt 16 Wie man sehen kann, gewinnt der advanced KI-Spieler häufiger als der einfache KI-Spieler. Zudem sind alle andern puts verschwunden. Da es sich bei diesem Spiel um eine reine Computersimulation handelt, war dieser Testansatz sehr einfach zu entwickeln und umzusetzen, da keine Benutzereingaben simuliert werden mussten und es keine Abhängigkeiten zu andern System gibt. Fabian Merki 05.11.2006 7 von 10
Startbefehle Client / Server Bitte öffnen Sie eine Shell und wechseln Sie ins src Verzeichnis. Server starten: ruby server.rb Clients starten (bitte 5 Mal ausführen) Windows: start ruby client.rb Unix / Linux: ruby client.rb & Nachdem ein Spiel gespielt wurde, werden die Clients automatisch geschlossen in der Konsole des Servers sieht man den Spielverlauf. Tests Alles in einer Rubyinstanz starten und KI-Spieler gegeneinander 100 Mal antreten lassen: ruby all.rb Unittests starten ruby unittest.rb Fabian Merki 05.11.2006 8 von 10
Zusammenfassung Ruby s Stärke liegt sicherlich in der Einfachheit Programme mit wenig Code zu schreiben. So benötigte ich für das ganze Spiel inklusive Unittests etc. gerade einmal ~15 KB benötigt. Würde man das Uno Spiel zum Beispiel in Java implementieren, wäre vermutlich das ant build file halb so gross und der Code selbst mindestens 2-3 so viel. Wenn man bedenkt, dass ich für die Client / Server Kommunikation gerade einmal ein paar Zeilen programmieren musste, so fragt man sich schon, weshalb in anderen Sprachen so viel Aufwand nötig ist. Auch die Verwendung von include DRbUndumped um verteilte Objekte zu produzieren, stellt Corba, RMI und ähnliche Protokolle komplett in den Schatten. Features, wie das Umbenennen einer Methode sowie das Erweitern einer Klasse, sind ein wenig gewöhnungsbedürftig, können aber durchaus hilfreich sein. Die Verwendung von attr resp. attr_accessor, um den Zugriff auf Objektvariablen zu definieren, oder die Konstrukte 10.times { }, liste.each {}, map.each_pair {... } sind natürlich sehr elegant und zeigen sehr schön, wie einfach Ruby sein kann. Ein grosser Nachteil hat Ruby aber schon: da es eine sehr dynamische, untypisierte Sprache ist, kann man formelle Programmierfehler nicht im Voraus sondern nur zur Laufzeit entdecken. Wie bereits erwähnt, lohnt sich der Einsatz von Unittests sehr vielleicht ist er in Ruby sogar überlebenswichtig. Übrigens gibt es die Möglichkeit Ruby typisiert zu Verwenden, wenn man den entsprechenden Inlude tätigt. Im Uno Spiel habe ich auf diese Möglichkeit verzichtet. Eines hat mich sehr erstaunt, gerade weil Ruby selbst sehr einfach ist. Die verfügbaren Grafikbibliotheken wie Tcl/Tk und andere sind nämlich extrem kompliziert und fast komplett undokumentiert. Dazu muss man noch diese meistens noch zusätzlich installieren. Mir gelang es zum Beispiel nicht in nützlicher Zeit ein Bild (mit allen Karten) einzulesen und teile davon auszuschneiden. Zwar ist die Klasse TkPhotoImage auf Methodenebene dokumentiert, jedoch fehlen Angaben zu den Map-Parametern komplett. Folgende Zeile habe ich zufälligerweise in einem Beispiel gefunden: image.copy(tmp2, 'subsample' => [0.5, 0.5]) Wie soll man hier bloss wissen, dass subsample existiert und was es für Auswirkungen hat? Ein Fenster mit ein paar Buttons und Labels war nicht all zu schwer zu programmieren. Für komplexere GUI Programmierung ist die Verwendung von Java sicherlich einfacher und man stösst nicht so rasch auf undokumentiere APIs. Da ich schon viele Stunden mit Ruby und GUIs verbraten habe, habe ich am Schluss auf ein Uno GUI verzichtet. Fabian Merki 05.11.2006 9 von 10
Tools und Frameworks Ruby http://www.ruby.org FreeRide Tcl/Tk Fabian Merki 05.11.2006 10 von 10