Bernhard Häussner

MacMahonMosaik Online Game

09.07.2009, 18:05
Mac Mohan Mosaik

Mac Mohan Mosaik

Da ich beim Känguru-Wettbewerb 2009 mitgemacht habe, habe ich auch das MacMahonMosaik in die Hände bekommen. Es ist ein kleines Rätsel ähnlich wie Sudoku. Weil es interessant zu Modellieren ist, habe ich das MacMahonMosaik als Online-Spiel erstellt. Hier ein paar Details zu Implementierung:

Modell

Jedes Quadrat im Spiel hat 4 Kanten, die in 3 verschiedenen Farben markiert sein können. Daher lässt sich jede Anordnung als 4-Tupel einer 3-Menge auffassen. Diese 81 Tupel wiederum können den natürlichen Zahlen von 0-80 zugeordnet werden. Somit habe ich als Objekte zunächst die 24 Quadrate (die anderen fallen weg, da sie durch Drehung erreicht werden können), die jeweils eine Zahl speichern. Sie müssen sich nun z.B. drehen können. Dazu wird die Zahl in das Tupel umgerechnet und die Einträge des Tupels um eine Stelle verschoben, wobei der letzte Eintrag der erste wird. Um zu Überprüfen, ob zwei Quadrate aneinander passen, dreht man eines der beiden zweimal und schaut dann einfach ob in der gewünschten Richtung die selben Farben stehen. Dadurch sieht die Prüffunktion im Javascript sehr kurz aus:

p.checkMatch=function(other,pos){
  var mycodes=this.getColorCodes(); // Tupel aus Zahl
  var o=other.clone(); // Neues Quadrat mit selber Zahl
  var othercodes=o.rotate().rotate().getColorCodes(); // anderes Q. 2x drehen
  return (mycodes.charAt(pos)==othercodes.charAt(pos)); //prüfen
}

Zusätzlich haben die Quadrate noch ein paar Darstellungsrelevante optionale Eigenschaften und Methoden. Ist ihnen ein <Canvas>-Element im HTML-Baum delegiert, können sie ihm die passenden Event-Handler für das interaktive Drehen und Bewegen zuweisen und eine graphische Repräsentation ihrer selbst zeichnen. Und sie speichern einige Daten für drag & drop.

Doch beim Ziehen und Ablegen kommt das zweite Objekt ins Spiel: Das Feld in dem die Quadrate abgelegt werden können. Wann immer man ein Quadrat bewegt, frag dieses Quadrat beim Gitter nach, ob es sich einrasten kann. Das Gitter gäbe dann die Pixel-Koordinaten des Einrastpunkts auf dem Bildschirm und die Koordinaten innerhalb des Gitters zurück. Wird das Quadrat dann fallen gelassen, registriert es sich im Gitter.

Das Gitter hat dazu ein zweidimensionales Feld hinterlegt, in dem es Pointer zu den abgelegten Quadraten speichern kann. Wenn jetzt ein Quadrat im Raster abgelegt wird und sich registriert hat, weist es das Raster an, sich zu revalidieren - Das Gitter ruft für jedes Quadrat und je seine vier Nachbarn die Prüffunktion auf. (Da das „Passen“ symmetrisch ist, ließe sich die Komplexität des Algorithmus vielleicht noch optimieren, doch das hätte wieder mehr Code zur Folge und bei 24 Einheiten...) Da das Gitter nur Zeiger zu den Quadraten speichert, ist bei einer Drehung eines Quadrats keine weitere Registrierung nötig. Falls also ein Quadrat angeklickt wird, rotiert es sich, aktualisiert die eigene Darstellung und gibt den Revalidierungsbefehl an das Raster.

Für die Darstellung hat das Gitter auch eine recht einfache Methode, die den Feldhintergrund rötlich färbt, sollte ein Fehler vorliegen und eine Gratulation meldet, sollten alle Quadrate an einem passenden Ort sein, also das Spiel gewonnen.

Ablaufdiagramm

Hier mal ein kleiner Flowchart. Man beginne das Lesen bei den Event-Handlern: (erstellt mit U+2500-U+257F: Box Drawing)

Quadrat
├ Zahl ◁──────┐ {Umrechnung}
├ (Tupel) ◁───┘
├ Drehen() ◀────────┐◁──┐─┐▷─────┐
├ Prüfen(Quadrat) ──◆───┊─┊──────┊──┐
├ [Darstellung]     │   │ │      │  │
│  ├ Visualisieren◁─┊───┊─┘      │  │
│  └ [Handler]      │   │        │  └──┐
│     ├ Klick ──────┊───┘        │     │
│     ├ Packen  ────┊────────────┊──┐  │
│     ├ Bewegen ◀───┊───┐        │  │  │
│     └ Ablegen ────┊─────────┬▷─┤  │  │
└ Kopieren() ►──────┘   │     │  │  │  ▼ bool
                     [XY,XY]  │  │  │  │
Gitter                  │     │  │  │  │
├ Feld[X,Y]             │     │  │  │  │
├ KoordinatenBei(XY) ───┘     │  │  │  │
├ Registrieren(Quadrat)  ◀─XY─┘  │  │  │
│ └ Un-Registrieren() ◀─────XY───┊──┘  │
├ Alle Prüfen()  ►────────┐─▷────│─────┘
└ Darstellung erneuern() ─┘  ◁───┘
  └ Färben(), Gratulation()

Wie das Gitter überprüft, ob auch am Rand rote Kanten liegen? Statt weiteren Code für den Rand zu basteln, wird das Gitter mit voll-roten Quadraten ohne graphische Repräsentation außerhalb das Gitters vorbelegt. Das hat den kleinen Nebeneffekt, das auch out-of-bounds-Fehler im Prüfalgorithmus vermieden werden, da ja z.B. bei negativen Gitterkoordinaten -1 stets ein solches „virtuelles“ Quadrat liegt.

Wie man also sieht, kann man an diesem Spiel schon einige lustige Code-Spar-Methoden benutzen. Eigentlich sparen sie nicht nur Code, sondern auch Nachdenken, wenn man sie „sieht“.

Javascript Umsetzung

JS-Closures habe ich in letzter Zeit immer mehr zu schätzen gelernt. (Man muss in PHP übrigens auch nicht mehr lange darauf verzichten) Sie lassen den Programmierer Funktionen, also mehr oder weniger Code, wie ein String oder eine Zahl an andere Funktionen übergeben oder in Variablen spichern, sodass mit anonymen Funktionsobjekten als Closures in JS eigene Kontrollstrukturen definiert werden können, wie diese zweifache for-Schleife, um über Koordinaten bzw. Zweidimensionales zu loopen:

function loopXY(w,h,cb) {
  for (var x=0;x<w;x++) {
    for (var y=0;y<h;y++) {
      cb(x,y);
    }
  }
}
var m=[];
loopXY(5,7,function(x,y){
  m.push("("+x+","+y+")");
});
alert(m);

Wie man sieht stehen in Javascript in den Closures alle äußeren Variablen zur Verfügung. Das wiederholte verwenden des Namens einer äußeren Variable führt trozdem noch dazu, dass in der Closure diese lokal verwendet wird und außen der alte Wert erhalten bleibt. Ein bischen gewöhnungsbedürftig ist die Verwendung von this in JS. Im oben gezeigten Code würde z.B. in der Schleife this auf window zeigen. Das lässt sich mit der Function.call oder der Function.apply-Funktion kompensieren, wie in diesen Code-Schnippseln für Event-Handler.

Für die Erstellung von Objekten (außer reinen Datenspeichern, die mit {} einfach erstellt werden können) verwende ich folgende Grundstruktur:

function Square(id) { // constructor
	this.id=id; // Eigenschaften initialisieren
}
(function(){
  var p=Square.prototype; // shortcut zum Prototype
  p.setId=function(id) { // einfacher Setter
    this.id=id;
    return this; // macht Setter chainable
  };
  var privat; // kann nur von den Objektmethoden gesehen werden
  function privatauch() {} // dito
})();
var s=new Square (3);

Allerdings bin ich mir nicht sicher, ob das wirklich besser ist, als alles in Konstruktoren zu packen.

Der Code des Onlinegames ist vielleicht nicht ganz so interessant zu lesen, wie es war ihn zu erstellen, aber ich denke, hinein schauen lohnt sich. Ich bin natürlich immer interessiert an weiteren Javascript-Techniken und Tipps zur Code-Gestaltung.

Kurze URL http://1-co.de/b/1E. Post to twitter

Kommentare

keine





 
Χρόνογραφ
© 2008-2014 by Bernhard Häussner - Impressum - Login
Kurz-Link zu dieser Seite: http://1-co.de/b/1E