Skip to content

Instantly share code, notes, and snippets.

@ralfw
Created July 6, 2013 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ralfw/5941505 to your computer and use it in GitHub Desktop.
Save ralfw/5941505 to your computer and use it in GitHub Desktop.
Tic Tac Toe mit JavaScript und Eventstore Inspiriert durch Mike Bilds Vorlage: https://gist.github.com/MikeBild/5926056
var BlackBox = function() {
var self = this;
self._events = [];
self.Record = function(event) {
self._events.push(event);
self.Recorded(event);
};
self.Replay = function() {
return self._events;
};
self.Recorded = function(event) {};
};
var Controller = function() {
var self = this;
Eventhandler_an_View_binden();
self.Anzeigen = function(spiel) {
spiel.Zuege.forEach(function(zug) {
document.getElementById(zug.Spielfeldkoordinate).innerText = zug.Spieler;
});
document.getElementById("situation").innerText = spiel.Spielstand;
document.getElementById("player").innerText = spiel.Spieler;
document.getElementById("round").innerText = spiel.Runde;
};
self.Gezogen = null;
function Eventhandler_an_View_binden() {
var spielbrett = Array.prototype.slice.call(document.getElementsByTagName("td"));
spielbrett.forEach(function (spielfeld) {
spielfeld.addEventListener('click', function () {
self.Gezogen(this.id);
});
});
}
};
var Domain = function(blackbox) {
var self = this;
self._blackbox = blackbox;
blackbox.Record({Name:"Spielerwechsel", Spieler:"X"});
self.Ziehen = function(spielfeldkoordinate) {
self.Validieren(spielfeldkoordinate, function() {
self.Zug_ausfuehren(spielfeldkoordinate);
self.Spieler_wechseln();
self.Spielstand_ermitteln();
self.Zug_ausgefuehrt();
});
};
self.Validieren = function(spielfeldkoordinate, weiter_bei_validem_Zug) {
var events = self._blackbox.Replay();
if (events.length > 0 && events[events.length-1].Name == "Spielende")
return;
if (linq.from(events).any(function(e){return e.Name=="Zug" && e.Spielfeldkoordinate==spielfeldkoordinate}))
return;
weiter_bei_validem_Zug();
};
self.Zug_ausfuehren = function(spielfeldkoordinate) {
var events = self._blackbox.Replay();
var aktueller_Spieler = events[events.length-1].Spieler;
self._blackbox.Record({Name:"Zug", Spielfeldkoordinate:spielfeldkoordinate, Spieler:aktueller_Spieler});
};
self.Spieler_wechseln = function() {
var events = self._blackbox.Replay();
var aktueller_Spieler = events[events.length-1].Spieler == "X" ? "O" : "X";
self._blackbox.Record({Name:"Spielerwechsel", Spieler:aktueller_Spieler});
};
self.Spielstand_ermitteln = function() {
var events = self._blackbox.Replay();
var zuege = linq.from(events).where(function(e){return e.Name=="Zug"}).list;
if (Pruefen_auf_Gewinn("X")) return;
if (Pruefen_auf_Gewinn("O")) return;
if (zuege.length == 9)
self._blackbox.Record({Name:"Spielende", Spielstand:"unentschieden"});
function Pruefen_auf_Gewinn(spieler) {
var solutions = [[11,12,13], [21,22,23], [31,32,33],
[11,22,33], [13,22,31], [11,21,31],
[12,22,32], [13,23,33]];
var spielerzuege = linq.from(zuege)
.where(function (e) {return e.Spieler == spieler})
.select(function (e) {return e.Spielfeldkoordinate})
.list;
for(var i=0; i<solutions.length; i++) {
if (linq.from(spielerzuege)
.intersect(solutions[i])
.list
.length == 3) {
self._blackbox.Record({Name:"Spielende", Spielstand:spieler+" gewinnt"});
return true;
}
}
return false;
}
};
self.Zug_ausgefuehrt = null;
};
<!DOCTYPE html>
<html>
<head>
<style>
table {
border: 4px solid black;
}
table tr td{
width: 50px;
height: 50px;
border: 1px solid black;
text-align: center;
vertical-align: middle;
}
</style>
</head>
<body>
<div id="board">
<table>
<tr>
<td id="31"></td>
<td id="32"></td>
<td id="33"></td>
</tr>
<tr>
<td id="21"></td>
<td id="22"></td>
<td id="23"></td>
</tr>
<tr>
<td id="11"></td>
<td id="12"></td>
<td id="13"></td>
</tr>
</table>
</div>
<div>
Round:
<div id="round">1</div>
Player:
<div id="player">X</div>
Situation:
<div id="situation"></div>
</div>
<script src="js/Linq.js"></script>
<script src="js/BlackBox.js"></script>
<script src="js/Controller.js"></script>
<script src="js/Domain.js"></script>
<script src="js/ReadModel.js"></script>
<script>
var blackbox = new BlackBox();
var controller = new Controller();
var domain = new Domain(blackbox);
var readModel = new ReadModel(blackbox);
controller.Gezogen = domain.Ziehen;
domain.Zug_ausgefuehrt = readModel.Generieren;
readModel.Spiel = controller.Anzeigen;
</script>
</body>
</html>
var Linq = function() {
var self = this;
self.from = function(list) {
return {
select: function(selector) {
var newList = new Array();
for(var i=0; i<list.length; i++)
newList.push(selector(list[i], i));
return self.from(newList);
},
where: function(predicate) {
var newList = new Array();
for(var i=0; i<list.length; i++)
if (predicate(list[i], i))
newList.push(list[i]);
return self.from(newList);
},
any: function(predicate) {
for(var i=0; i<list.length; i++)
if (predicate(list[i], i))
return true;
return false;
},
intersect: function(list2) {
var intersection = new Array();
for(var i=0; i<list.length; i++)
for(var j=0; j<list2.length; j++)
if (list[i]==list2[j])
intersection.push(list[i]);
return self.from(intersection);
},
list: list
}
}
}
var linq = new Linq();
var ReadModel = function(blackbox) {
var self = this;
self._blackbox = blackbox;
self.Generieren = function() {
var events = self._blackbox.Replay();
var zuege = linq.from(events)
.where(function(e){return e.Name=="Zug"})
.select(function(e, i){return {
Spielfeldkoordinate: e.Spielfeldkoordinate,
Spieler: i % 2 == 0 ? "X" : "O"
}})
.list;
var aktueller_spieler = events[events.length-1].Name == "Spielerwechsel"
? events[events.length-1].Spieler
: "-";
var spielstand = events[events.length-1].Name == "Spielende"
? events[events.length-1].Spielstand
: "-";
self.Spiel({Zuege: zuege, Spielstand: spielstand, Spieler: aktueller_spieler, Runde: zuege.length+1});
};
self.Spiel = null;
};
@MikeBild
Copy link

MikeBild commented Jul 7, 2013

cool + spannend!

Allgemein:
Naming: Gute Mischung aus technischem und fachlichem Naming.
Struktur: Mir ist aufgefallen: Du bist "außen" (controller, domain, readmodel) eher technisch und "innen" (züge, spieler, usw) fachlich. Ist bei mir -soweit es geht und zumindest gewünscht ;)- anders herum.

BootStrap in index.html:
Pro: Abhängigkeiten werden expilzit an einer Stelle sichtbar.
Con:
A) wäre vielleicht noch etwas besser im Controller aufgehoben?!? So bleibt das HTML Dokument frei von Script.
B) Mir reichen die "includes" (script source) und immediate functions um Abhängigkeiten sichtbar zu machen. Fehlt was, "knallt" es zur Laufzeit so und so sofort. Austauschbarkeit: Wenn absolut nötig, reicht script source austauschen.

domain.js:
Ziehen finde ich nett.
Insgesamt ist in deinem Code die Verwendung der Projektionen noch anschaulicher. Jede Methode verwendet "eigene" Projektionen aus BlackBox. Die BlackBox wird indirekt als "unabhängige continuation" über die Methoden verwendet. So soll es sein. "Viele" Queries sind natürlich die Folge. Jetzt auch mit intersect ;) - cool - mit 3 Vergleichen ist natürlich optimaler ;)

details: Ich würde alle anderen Methoden "private", d.h. nicht auf self/this, binden.

linq.js
NICE

readmodel.js
Logik/Berechnungen auf "Anzahl der Events" - okay.

Merkst du auch, wie "nah" Projektionen auf Events beieinander liegen? Für mich daher untrennbar.

Insgesamt machst du einiges anders. Mehr "statischer" und damit expliziter Datenfluss und "aufbauender" Prozess/Continuation statt Event-Driven. Auch gut so.

@MikeBild
Copy link

MikeBild commented Jul 7, 2013

... achso
... hab auch "gesäubert" -> https://gist.github.com/MikeBild/5926056
... Single-Page-App nochmals in Git published -> https://github.com/MikeBild/TicTacToe/tree/master/SPA
... und eine NODE.JS Client-Server Variante erstellt -> https://github.com/MikeBild/TicTacToe/tree/master/ClientServer

PS: TTT-Client-Server-Coolness-Faktor - Copy&Paste von Client-Side-Game-Logik auf Server-Side-Game-Logik ;) Sind nur ein paar Module-Exports und natürlich die HTTP Kommunikation (bisher sync) angepasst.

NEXT: Ich bau es mal noch "async", um die Vorteile von Append-Only-Model und Skalierung besser zeigen zu können.

@ralfw
Copy link
Author

ralfw commented Jul 7, 2013

Struktur: Klar bin ich "außen" (Membran der Softwarezelle) technisch und innen fachlich. So sollte es sein, würde ich sagen. Die Domain ("innen") repräsentiert ja die Fachlichkeit. Dafür wird Software gemacht :-) Ubiquitous language lässt grüßen.

JavaScript: Ich bin da ja kein Profi. Ob man sich <Script> sparen kann oder so, weiß ich nicht. Dito bei "private" Methoden. Da war ich mir unsicher, wie das am besten ausgedrückt wird. Ich will in denen ja auch auf den Zustand einer Instanz zugreifen.

Projektionen: Das ist mir wichtig zu zeigen. Jeder kann den einen Event-Strom so projizieren/analysieren, wie er mag. Damit sind die einzelnen Teile maximal unabhängig. Zustand bzw. gemeinsame Datenstrukturen sind Optimierungen. Die kann man später einführen, wenn man mag.

Um die Projektionen unabhängiger zu halten, habe ich sogar die Events gegenüber der C#-Variante im Blog "aufgeblasen". Es gibt verschiedene und mit mehr Infos. An jedem Zug hängt jetzt z.B. der Spieler. Man könnte zwar alles aus den Zügen immer wieder ableiten - aber sprechender finde ich es, wenn auch "Schlüsse" (z.B. ein Spielerwechsel oder Spielende) mit protokolliert werden.

Damit schützt man die App gegen Veränderungen in der Domänenlogik. Denn die könnten in der Zukunft zu Neuinterpretationen alter Events führen. Das wäre nicht gut. Ein EventStore speichert also nicht nur Veränderungen, sondern auch Entscheidungen. Sozusagen geronnene Domänenlogik.

Das bisschen Linq hatte ich mal Lust zu basteln, um mich an JS zu versuchen und den Code self-contained zu halten. Aber es gibt natürlich auch "offizielle" Libs dazu, z.B. http://linqjs.codeplex.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment