React Anfänger-Tutorial: Erstellen eines Brettspiels von Grund auf neu

In diesem React.js-Tutorial erfahren Sie, wie Sie mit React.js-Komponenten und -Komponenten ein Brettspiel von Grund auf neu erstellen Unterstrich.js. Es wurde ursprünglich gepostet hier.


Reagieren ist „eine Javascript-Bibliothek zum Erstellen von Benutzeroberflächen“. Wenn Sie es noch nicht getan haben, empfehle ich Ihnen dringend, es sich anzusehen Präsentation von Pete Hunt zu den Designprinzipien von React. React ist eine relativ einfache Bibliothek, insbesondere im Vergleich zu vollwertigen MVC-Frameworks wie Angular, Ember, Backbone und anderen. Es macht Spaß, damit zu arbeiten, also fangen wir an.

Hinweis: In diesem Beitrag sind Codefragmente verstreut. Um den Quellcode für die endgültige Anwendung zu sehen, besuchen Sie meine Github-Repository.

Heute werden wir das Brettspiel implementieren gehen. Wenn Sie nicht wissen, wie man spielt, ist das in Ordnung. Alles, was Sie vorerst wissen müssen, ist, dass die Spieler abwechselnd Steine ​​auf Kreuzungen des Gitters des Bretts platzieren, um die Steine ​​ihres Gegners zu erobern und das größte Territorium zu beanspruchen. Werfen Sie einen Blick auf die Live Vorschau um eine Vorstellung davon zu bekommen, was wir bauen werden.

Lass uns beginnen mit index.html.

<!DOCTYPE html>
<html>
  <head>
    <title>React Go Tutorial</title>
    <link type="text/css" href="style.css" rel="stylesheet" />
  </head>
  <body>
    <div id="main">
    </div>
    <script src="//cdnjs.cloudflare.com/ajax/libs/react/0.8.0/react.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/react/0.8.0/JSXTransformer.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script>
    <script type="text/javascript" src="board.js"></script>
    <script type="text/jsx" src="go.js"></script>
  </body>
</html>

Hier ist nichts zu überraschend. Beachten Sie, dass wir einschließen JSXTransformer.js. Dies ist der Präprozessor von React. Es ermöglicht uns, eine spezielle benutzerdefinierte Syntax zu verwenden, um unsere React-Ansichten zu beschreiben, die eher dem Schreiben von HTML als von Javascript ähnelt. Während der Entwicklung ist es in Ordnung, sich auf den Client zu verlassen, um Ihre React-Dateien vorzuverarbeiten, aber wenn Sie in die Produktion gehen, stellen Sie sicher, dass Sie es tun Kompilieren Sie diese Assets vor. Bitte beachten Sie, dass die obige Abhängigkeit von Unterstrich.js ist nicht notwendig, um React-Apps zu erstellen, aber ich verwende es in meiner Anwendungslogik, weil es einige nette Hilfsfunktionen bietet, die uns Javascript nicht standardmäßig bietet.

Anwendungslogik zuerst

Oben füge ich beide hinzu board.js und go.js. board.js enthält die gesamte Spiellogik. Es ist immer eine gute Idee, sich zu trennen Anwendung Logik aus Präsentation Logik, und React fördert diese Praxis. Beachten Sie, dass board.js hat überhaupt keine Abhängigkeit von React: Es ist nur Vanille-Javascript, das wir kennen und lieben.

/*
 * board.js - Game logic for the board game Go
 */
var Board = function(size) {
  this.current_color = Board.BLACK;
  this.size = size;
  this.board = this.create_board(size);
  this.last_move_passed = false;
  this.in_atari = false;
  this.attempted_suicide = false;
};

Board.EMPTY = 0;
Board.BLACK = 1;
Board.WHITE = 2;

/*
 * Returns a size x size matrix with all entries initialized to Board.EMPTY
 */
Board.prototype.create_board = function(size) {
  var m = [];
  for (var i = 0; i < size; i++) {
    m[i] = [];
    for (var j = 0; j < size; j++)
    m[i][j] = Board.EMPTY;
  }
  return m;
};

/*
 * Switches the current player
 */
Board.prototype.switch_player = function() {
  this.current_color = 
    this.current_color == Board.BLACK ? Board.WHITE : Board.BLACK;
};

/*
 * At any point in the game, a player can pass and let his opponent play
 */
Board.prototype.pass = function() {
  if (this.last_move_passed)
      this.end_game();
  this.last_move_passed = true;
  this.switch_player();
};

/*
 * Called when the game ends (both players passed)
 */
Board.prototype.end_game = function() {
  console.log("GAME OVER");
};

/*
 * Attempt to place a stone at (i,j). Returns true iff the move was legal
 */
Board.prototype.play = function(i, j) {
  console.log("Played at " + i + ", " + j);   
  this.attempted_suicide = this.in_atari = false;

  if (this.board[i][j] != Board.EMPTY)
    return false;

  var color = this.board[i][j] = this.current_color;
  var captured = [];
  var neighbors = this.get_adjacent_intersections(i, j);
  var atari = false;

  var self = this;
  _.each(neighbors, function(n) {
    var state = self.board[n[0]][n[1]];
    if (state != Board.EMPTY && state != color) {
      var group = self.get_group(n[0], n[1]);
      console.log(group);
      if (group["liberties"] == 0)
        captured.push(group);
      else if (group["liberties"] == 1)
        atari = true;
    }
  });

  // detect suicide
  if (_.isEmpty(captured) && this.get_group(i, j)["liberties"] == 0) {
    this.board[i][j] = Board.EMPTY;
    this.attempted_suicide = true;
    return false;
  }

  var self = this;
  _.each(captured, function(group) {
    _.each(group["stones"], function(stone) {
      self.board[stone[0]][stone[1]] = Board.EMPTY;
    });
  });

  if (atari)
    this.in_atari = true;

  this.last_move_passed = false;
  this.switch_player();
  return true;
};

/*
 * Given a board position, returns a list of [i,j] coordinates representing
 * orthagonally adjacent intersections
 */
Board.prototype.get_adjacent_intersections = function(i , j) {
  var neighbors = []; 
  if (i > 0)
    neighbors.push([i - 1, j]);
  if (j < this.size - 1)
    neighbors.push([i, j + 1]);
  if (i < this.size - 1)
    neighbors.push([i + 1, j]);
  if (j > 0)
    neighbors.push([i, j - 1]);
  return neighbors;
};

/*
 * Performs a breadth-first search about an (i,j) position to find recursively
 * orthagonally adjacent stones of the same color (stones with which it shares
 * liberties). Returns null for if there is no stone at the specified position,
 * otherwise returns an object with two keys: "liberties", specifying the
 * number of liberties the group has, and "stones", the list of [i,j]
 * coordinates of the group's members.
 */
Board.prototype.get_group = function(i, j) {
  var color = this.board[i][j];
  if (color == Board.EMPTY)
    return null;

  var visited = {}; // for O(1) lookups
  var visited_list = []; // for returning
  var queue = [[i, j]];
  var count = 0;

  while (queue.length > 0) {
    var stone = queue.pop();
    if (visited[stone])
      continue;

    var neighbors = this.get_adjacent_intersections(stone[0], stone[1]);
    var self = this;
    _.each(neighbors, function(n) {
      var state = self.board[n[0]][n[1]];
      if (state == Board.EMPTY)
        count++;
      if (state == color)
        queue.push([n[0], n[1]]);
    });

    visited[stone] = true;
    visited_list.push(stone);
  }

  return {
    "liberties": count,
    "stones": visited_list
  };
}

Eine Instanz der Board Die Klasse hat mehrere Attribute, die beschreiben, wie ein Go-Spiel zu einem bestimmten Zeitpunkt aussieht. Dies ist ein gängiges Paradigma in React: Machen Sie sich mit dem Erstellen von Modellen vertraut, die über Attribute verfügen, die von sich selbst zum Erstellen Ihrer Ansichten verwendet werden können. Werfen wir einen Blick darauf, wie a Board wird repräsentiert.

  • Board.size speichert eine Ganzzahl, die die Abmessungen des Spielbretts darstellt. Go-Spiele werden auf einem quadratischen Raster gespielt, das normalerweise aus 19 × 19-Kreuzungen besteht, aber Anfänger spielen manchmal auf kleineren 9 × 9- und 13 × 13-Brettern.
  • Board.current_color speichert eine Ganzzahl, die angibt, wer an der Reihe ist. Da der Spieler mit den schwarzen Steinen zuerst spielt, initialisieren wir this.current_color zu Board.BLACK.
  • Board.board ist eine ganzzahlige Matrix, die speichert, welche Farbsteine ​​welche Felder belegen. Da das Board leer beginnt, initialisieren wir jede Zelle auf Board.EMPTY.
  • Ein Go-Spiel endet, wenn beide Spieler nacheinander an der Reihe sind. Wenn ein Spieler an der Reihe ist, setzen wir Board.last_move_passed damit wir erkennen können, dass das Spiel beendet ist, wenn der nächste Zug auch ein Pass ist.
  • Wenn ein Spieler seinen Gegner bedroht, setzen wir die Flagge Board.in_atari auf true, damit wir den Spieler in Gefahr warnen können. In Go gilt dies als höflich.
  • Zum Schluss setzen wir die Board.attempted_suicide kennzeichnen, wenn ein Benutzer einen ungültigen Zug gemacht hat – einen, der Selbstmord für seine Figur bedeuten würde.

Der lustige Teil: React-Komponenten bauen

Jetzt haben wir eine gute Darstellung des Brettspiels in reinem Javascript. Wir können die Methoden anwenden Board.pass() und Board.play(i, j) um den Status des Spiels zu ändern. Alle anderen Methoden werden nur von der verwendet Board Klasse intern. Beginnen wir damit, unsere Benutzeroberfläche mit React zusammenzustellen.

Was folgt, sind mehrere Segmente von go.js, wo wir unsere React-Komponenten bauen. Um die Datei vollständig anzuzeigen, schau es dir auf Github an. Wir beginnen die Datei mit einem Kommentar, der erklärt, dass diese Datei von vorverarbeitet werden soll JSX. Außerdem erstellen wir eine Konstante namens GRID_SIZEdas die Pixelabmessungen eines Gitterquadrats auf unserem Spielbrett speichert.

/** @jsx React.DOM */ 
var GRID_SIZE = 40;

Als nächstes bauen wir die erste React-Komponente aus. Das ist ziemlich einfach. Es stellt eine einzelne Gitterkreuzung auf dem Go-Brett dar.

var BoardIntersection = React.createClass({
  handleClick: function() {
    if (this.props.board.play(this.props.row, this.props.col))
        this.props.onPlay();
  },
  render: function() {
    var style = {
      top: this.props.row * GRID_SIZE,
      left: this.props.col * GRID_SIZE
    };

    var classes = "intersection ";
    if (this.props.color != Board.EMPTY)
      classes += this.props.color == Board.BLACK ? "black" : "white";

    return (
      <div onClick={this.handleClick} 
           className={classes} style={style}></div>
    );
  }
});

BoardIntersection hat mehrere Eigenschaften, die wir übergeben können, wenn wir eine Instanz initialisieren:

  • BoardIntersection.board ist die Instanz von Board wir vertreten.
  • BoardIntersection.color stellt dar, welcher Farbstein, falls vorhanden, diesen Schnittpunkt einnimmt.
  • BoardIntersection.row und BoardIntersection.col repräsentieren die (i,j) Lage dieser Kreuzung.
  • BoardIntersection.onPlay ist eine Funktion, die wir übergeben werden und die immer dann ausgeführt werden soll, wenn ein Zug im Spiel ausgeführt wird Board. Wir rufen diese Funktion immer dann auf, wenn ein Spieler auf die Kreuzung klickt.

Als nächstes bauen wir die Komponente, die das Spielbrett darstellt.

var BoardView = React.createClass({
  render: function() {
    var intersections = [];
    for (var i = 0; i < this.props.board.size; i++)
      for (var j = 0; j < this.props.board.size; j++)
        intersections.push(BoardIntersection({
          board: this.props.board,
          color: this.props.board.board[i][j],
          row: i,
          col: j,
          onPlay: this.props.onPlay
        }));
    var style = {
      width: this.props.board.size * GRID_SIZE,
      height: this.props.board.size * GRID_SIZE
    };
    return <div style={style} id="board">{intersections}</div>;
  }
});

BoardView hat nur zwei Eigenschaften, die wir verwenden werden: BoardView.board und BoardView.onPlay. Diese Eigenschaften spielen hier die gleiche Rolle wie in BoardIntersection. In dem render Methode dieser Komponente erstellen wir nxn Instanzen davon BoardIntersection und fügen Sie sie jeweils als untergeordnete Elemente hinzu.

Als Nächstes erstellen wir ein paar weitere Komponenten: eine zum Anzeigen von Warnmeldungen und eine andere, die eine Schaltfläche bereitstellt, mit der Sie an der Reihe sind.

var AlertView = React.createClass({
  render: function() {
    var text = "";
    if (this.props.board.in_atari)
      text = "ATARI!";
    else if (this.props.board.attempted_suicide)
      text = "SUICIDE!";

      return (
        <div id="alerts">{text}</div>
      );
  }
});

var PassView = React.createClass({
  handleClick: function(e) {
    this.props.board.pass();
  },
  render: function() {
    return (
      <input id="pass-btn" type="button" value="Pass" 
        onClick={this.handleClick} />
    );
  }
});

Schließlich bauen wir eine Komponente, um alle unsere Unterkomponenten zu verpacken. Wir initialisieren eine Instanz unseres Modells und rufen auf React.renderComponent um eine Komponente an ein DOM-Element zu binden.

var ContainerView = React.createClass({
  getInitialState: function() {
    return {'board': this.props.board};
  },
  onBoardUpdate: function() {
    this.setState({"board": this.props.board});
  },
  render: function() {
    return (
      <div>
        <AlertView board={this.state.board} />
        <PassView board={this.state.board} />
        <BoardView board={this.state.board} 
          onPlay={this.onBoardUpdate.bind(this)} />
      </div>
    )
  }
});
var board = new Board(19);

React.renderComponent(
  <ContainerView board={board} />,
  document.getElementById('main')
);

Das ContainerView ist unsere einzige zustandsbehaftete Komponente. Es hat genau eine Eigenschaft seines Zustands: boarddie auf die initialisiert wird board über seine an ihn weitergegeben props. Wir übergeben eine Callback-Funktion namens this.onBoardUpdate zum BoardViewdamit wir benachrichtigt werden können, wenn sich der Vorstand geändert hat.

Wie das alles funktioniert

In dem onBoardUpdate Rückruf, wir rufen an this.setState, die React benachrichtigt, dass sich unser Modell geändert hat, und React sollte dann unsere Komponente neu rendern, damit sie den aktuellen Modellzustand widerspiegelt. Hier kommt die Magie von React ins Spiel: Wir können das jedes Mal naiv so tun, wenn wir anrufen this.setStateReact ersetzt unser DOM-Element durch das, was von unseren Komponenten zurückgegeben wurde render Methode. In der Praxis ist das alles, was Sie wissen müssen, und zum größten Teil können wir so glücklich weiterdenken.

In der Praxis ist es viel zu teuer, all diese DOM-Manipulationen tatsächlich jedes Mal durchzuführen, wenn sich der Anwendungsstatus ändert. Hinter den Kulissen berechnet React also tatsächlich den minimalen Satz von Änderungen in der virtuellen DOM-Darstellung, die von einer Komponente zurückgegeben wird render Methode jedes Mal setState aufgerufen wird, führt dann nur diese Aktualisierungen durch. In unserem Fall bedeutet das normalerweise nur, einen einzelnen Klassennamen von a zu ändern <div> das stellt eine Brettkreuzung dar, oder möglicherweise mehrere, wenn Sie die Steine ​​Ihres Gegners erobern.

React vereinfacht den Prozess des Schreibens von Anwendungs-UIs, da wir nicht darüber nachdenken müssen, wie sich unser Modell im Laufe der Zeit ändert und wie unsere Ansicht inkrementell reagiert, während gleichzeitig nur geringfügige Leistungseinbußen entstehen. Es macht wirklich Spaß, damit zu arbeiten, und ich hoffe, dass es an Zugkraft gewinnt und ein Paradigma setzt, das sich in der Javascript-MVC-Szene vorwärts bewegt.


Über den Autor


Chris LaRose ist ein Softwareentwickler, der mit vielen Sprachen wie Python, C, Java, Ruby, JavaScript und SQL vertraut ist. Du kannst besuchen sein Blog für weitere Tipps und Tricks.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *