Przeglądarkowa gra wieloosobowa Tic Tac Toe w React
Published: 2024-03-10 22:29:55

**Ten post, pochodzący z naszych archiwów, przeprowadzi Cię przez kroki tworzenia gry Tic Tac Toe w React, ale pamiętaj, że wersje używanych bibliotek nie są już najnowsze.** W szczególności, artykuł ten wykorzystuje wersję 1 naszego [React SDK](https://www.pubnub.com/docs/sdks/react), ale wszystkie kroki i zasady wymienione poniżej są nadal aktualne.

[Gra](https://www.pubnub.com/solutions/gaming/) w kółko i krzyżyk to kwintesencja dzieciństwa. Wszystko, czego wymaga, to coś do pisania i coś, czym można pisać. Ale co, jeśli chcesz zagrać z kimś, kto jest w innej lokalizacji? W takim przypadku musisz użyć aplikacji, która połączy ciebie i innego gracza z grą.

Aplikacja musi zapewniać wrażenia w czasie rzeczywistym, aby każdy twój ruch był natychmiast widoczny dla drugiego gracza i odwrotnie. Jeśli aplikacja nie zapewnia takich wrażeń, to ty i wiele innych osób prawdopodobnie nie będzie już z niej korzystać.

Jak więc deweloper może zapewnić [połączone doświadczenie](https://www.pubnub.com/blog/connected-shared-experiences-a-developers-guide/), w którym gracze mogą grać w kółko i krzyżyk lub w dowolną inną grę, bez względu na to, gdzie się znajdują?

Koncepcje gier wieloosobowych w czasie rzeczywistym
---------------------------------------------------

Istnieje kilka sposobów na zapewnienie infrastruktury czasu rzeczywistego dla gier wieloosobowych. Można pójść drogą budowania własnej infrastruktury od podstaw, korzystając z technologii i protokołów open-source, takich jak [Socket.IO](https://www.pubnub.com/learn/glossary/what-is-socketio/), [SignalR](https://pubnub.com/guides/what-is-signalr/) lub [WebSockets](https://www.pubnub.com/learn/glossary/what-is-websocket/).

Chociaż może się to wydawać atrakcyjną drogą, napotkasz kilka problemów; jednym z nich jest skalowalność. Obsługa 100 użytkowników nie jest trudna, ale jak poradzić sobie z ponad 100 000 użytkowników? Poza kwestiami związanymi z infrastrukturą, wciąż musisz martwić się o utrzymanie swojej gry.

Koniec końców, jedyną rzeczą, która się liczy, jest zapewnienie graczom wspaniałych wrażeń z gry. Ale jak rozwiązać problem infrastruktury? Tutaj z pomocą przychodzi PubNub.

PubNub zapewnia infrastrukturę czasu rzeczywistego do zasilania dowolnej aplikacji za pośrednictwem globalnej [sieci strumieni danych](https://pubnub.com/products/pubnub-platform/). Dzięki [ponad 70 zestawom SDK,](https://pubnub.com/docs/) w tym najpopularniejszym językom programowania, PubNub upraszcza wysyłanie i odbieranie wiadomości do dowolnego urządzenia w czasie **poniżej 100 ms**. Jest bezpieczny, skalowalny i niezawodny, dzięki czemu nie musisz martwić się o tworzenie i utrzymywanie własnej infrastruktury.

Aby pokazać, jak łatwo jest stworzyć grę wieloosobową przy użyciu PubNub, zbudujemy prostą grę React tic tac toe przy użyciu [PubNub React SDK](https://pubnub.com/docs/sdks/javascript/react). W tej grze dwóch graczy połączy się z unikalnym kanałem gry, gdzie będą grać przeciwko sobie. Każdy ruch wykonany przez gracza zostanie opublikowany na kanale, aby zaktualizować planszę drugiego gracza w czasie rzeczywistym.

Przegląd aplikacji
------------------

Oto jak będzie wyglądać nasza aplikacja, gdy ją ukończymy.

![Screen shot of the React Tic Tac Toe Game](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpy/5d2874d8b7c577a9ce9a5acc6ade755f/React-tic-tac-toe-overview.png "Screen shot of the React Tic Tac Toe Game")Gracze najpierw dołączają do lobby, gdzie mogą utworzyć kanał lub dołączyć do kanału. Jeśli gracz utworzy kanał, otrzyma _identyfikator pokoju_ do współdzielenia z innym graczem. Gracz, który utworzył kanał, staje się _graczem X_ i wykona pierwszy ruch po rozpoczęciu gry.

[![Create a room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif "Create a room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif)

Gracz, który dołączy do kanału z _identyfikatorem_ pokoju, który otrzymał, staje się graczem _O_. Gracze mogą dołączać do kanałów tylko wtedy, gdy w kanale znajduje się jedna inna osoba. Jeśli na kanale znajduje się więcej niż jedna osoba, gra na tym kanale jest w toku i gracz nie będzie mógł do niej dołączyć. Gra rozpoczyna się, gdy na kanale znajduje się dwóch graczy.

[![Join the room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif "Join the room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif)

Na koniec gry wynik zwycięzcy jest zwiększany o jeden punkt. Jeśli gra zakończy się remisem, żaden z graczy nie otrzyma punktu. _Graczowi X_ wyświetlany jest komunikat modalny z prośbą o rozpoczęcie nowej rundy lub zakończenie gry. Jeśli gracz _X_ kontynuuje grę, plansza resetuje się do nowej rundy. W przeciwnym razie gra kończy się i obaj gracze wracają do lobby.

[![Exit to lobby](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif " Exit to lobby")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif)

Konfiguracja lobby
------------------

Zanim skonfigurujemy lobby, zarejestruj darmowe konto PubNub, aby uzyskać bezpłatne klucze Pub/Sub API z [panelu administracyjnego PubNub](https://admin.pubnub.com).

Po otrzymaniu kluczy wstaw je do konstruktora **App.js**.

```js
// App.js
import React, { Component } from 'react';
import Game from './Game';
import Board from './Board';
import PubNubReact from 'pubnub-react';
import Swal from "sweetalert2";
import shortid  from 'shortid';
import './Game.css';

class App extends Component {
  constructor(props) {
    super(props);
    // REPLACE with your keys
    this.pubnub = new PubNubReact({
      publishKey: "YOUR_PUBLISH_KEY_HERE",
      subscribeKey: "YOUR_SUBSCRIBE_KEY_HERE"
    });

    this.state = {
      piece: '', // X or O
      isPlaying: false, // Set to true when 2 players are in a channel
      isRoomCreator: false,
      isDisabled: false,
      myTurn: false,
    };

    this.lobbyChannel = null; // Lobby channel
    this.gameChannel = null; // Game channel
    this.roomId = null; // Unique id when player creates a room
    this.pubnub.init(this); // Initialize PubNub
  }

  render() {
    return ();
    }
  }

  export default App;
```

Również w konstruktorze inicjalizowane są obiekty stanu i zmienne. Omówimy te obiekty i zmienne, gdy pojawią się w całym pliku. Wreszcie, zainicjowaliśmy PubNub na końcu konstruktora.

Wewnątrz metody **renderowania** i w instrukcji **return** dodajemy znaczniki dla komponentu Lobby.

```js
return (
    <div>
      <div className="title">
        <p> React Tic Tac Toe </p>
      </div>

      {
        !this.state.isPlaying &&
        <div className="game">
          <div className="board">
            <Board
                squares={0}
                onClick={index => null}
              />

            <div className="button-container">
              <button
                className="create-button "
                disabled={this.state.isDisabled}
                onClick={(e) => this.onPressCreate()}
                > Create
              </button>
              <button
                className="join-button"
                onClick={(e) => this.onPressJoin()}
                > Join
              </button>
            </div>

          </div>
        </div>
      }

      {
        this.state.isPlaying &&
        <Game
          pubnub={this.pubnub}
          gameChannel={this.gameChannel}
          piece={this.state.piece}
          isRoomCreator={this.state.isRoomCreator}
          myTurn={this.state.myTurn}
          xUsername={this.state.xUsername}
          oUsername={this.state.oUsername}
          endGame={this.endGame}
        />
      }
    </div>
);
```

Komponent Lobby składa się z tytułu, pustej planszy do gry w kółko i krzyżyk (nic się nie dzieje, gdy gracz naciska kwadraty) oraz przycisków "Utwórz_"_ i "Dołącz_"_. Ten komponent jest wyświetlany tylko wtedy, gdy wartość stanu _isPlaying_ jest fałszywa. Jeśli jest ustawiona na true, gra się rozpoczęła, a komponent zostanie zmieniony na komponent Game, który omówimy w drugiej części samouczka.

Komponent Board jest również częścią komponentu Lobby. Wewnątrz komponentu [Board](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Board.js) znajduje się komponent [Square](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Square.js). Nie będziemy szczegółowo omawiać tych dwóch komponentów, aby skupić się na komponentach Lobby i Game.

Gdy gracz naciśnie przycisk "Utwórz", przycisk ten jest wyłączony, więc gracz nie może utworzyć wielu kanałów. Przycisk "Dołącz" nie jest wyłączony, na wypadek gdyby gracz zdecydował się dołączyć do kanału. Po naciśnięciu przycisku "Create" wywoływana jest metoda **onPressCreate()**.

### Tworzenie kanału

Pierwszą rzeczą, którą robimy w **onPressCreate** () jest wygenerowanie losowego identyfikatora łańcucha, który jest obcięty do 5 znaków. Robimy to za pomocą funkcji [**shortid()**](https://www.npmjs.com/package/shortid). Dołączamy ciąg do "_tictactoelobby-",_który będzie unikalnym kanałem lobby subskrybowanym przez graczy.

```js
// Create a room channel
onPressCreate = (e) => {
  // Create a random name for the channel
  this.roomId = shortid.generate().substring(0,5);
  this.lobbyChannel = 'tictactoelobby--' + this.roomId; // Lobby channel name

  this.pubnub.subscribe({
    channels: [this.lobbyChannel],
    withPresence: true // Checks the number of people in the channel
  });
}
```

Aby zapobiec dołączeniu więcej niż dwóch graczy do danego kanału, używamy funkcji [Presence](https://pubnub.com/docs/sdks/javascript/react). Później przyjrzymy się logice sprawdzania zajętości kanału.

Gdy gracz zasubskrybuje kanał lobby, wyświetlany jest modal z identyfikatorem pokoju, aby inny gracz mógł dołączyć do tego kanału.

[![Share the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png "Share the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png)

Ten modal i wszystkie modale używane w tej aplikacji są tworzone przez [**SweetAlert2**](https://sweetalert2.github.io/#download) w celu zastąpienia domyślnych wyskakujących okienek JavaScript alert().

```js
// Inside of onPressCreate()
// Modal
Swal.fire({
  position: 'top',
  allowOutsideClick: false,
  title: 'Share this room ID with your friend',
  text: this.roomId,
  width: 275,
  padding: '0.7em',
  // Custom CSS to change the size of the modal
  customClass: {
      heightAuto: false,
      title: 'title-class',
      popup: 'popup-class',
      confirmButton: 'button-class'
  }
})
```

Pod koniec **onPressCreate()** zmieniamy wartości stanu, aby odzwierciedlić nowy stan aplikacji.

```js
this.setState({
  piece: 'X',
  isRoomCreator: true,
  isDisabled: true, // Disable the 'Create' button
  myTurn: true, // Player X makes the 1st move
});
```

Gdy gracz utworzy pokój, musi poczekać, aż inny gracz dołączy do tego pokoju. Przyjrzyjmy się logice dołączania do pokoju.

### Dołączanie do kanału

Gdy gracz naciśnie przycisk "Dołącz", wywoływana jest funkcja **onPressJoin()**. Graczowi wyświetlany jest modal z prośbą o wprowadzenie _identyfikatora pokoju_ w polu wprowadzania.

[![Enter the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png "Enter the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png)

Jeśli gracz wpisze identyfikator _pokoju_ i naciśnie przycisk "OK", wywoływana jest metoda **joinRoom(value** ), gdzie **value** to _identyfikator pokoju_. Metoda ta nie jest wywoływana, jeśli pole wejściowe jest puste lub jeśli gracz naciśnie przycisk "Anuluj".

```js
// The 'Join' button was pressed
onPressJoin = (e) => {
  Swal.fire({
    position: 'top',
    input: 'text',
    allowOutsideClick: false,
    inputPlaceholder: 'Enter the room id',
    showCancelButton: true,
    confirmButtonColor: 'rgb(208,33,41)',
    confirmButtonText: 'OK',
    width: 275,
    padding: '0.7em',
    customClass: {
      heightAuto: false,
      popup: 'popup-class',
      confirmButton: 'join-button-class',
      cancelButton: 'join-button-class'
    }
  }).then((result) => {
    // Check if the user typed a value in the input field
    if(result.value){
      this.joinRoom(result.value);
    }
  })
}
```

Pierwszą rzeczą, którą robimy w **joinRoom** () jest dołączenie **wartości** do '_tictactoelobby-',_podobnie jak to zrobiliśmy w **onPressCreate()**.

```js
// Join a room channel
joinRoom = (value) => {
  this.roomId = value;
  this.lobbyChannel = 'tictactoelobby--' + this.roomId;
}
```

Zanim gracz zasubskrybuje kanał lobby, musimy sprawdzić całkowitą zajętość kanału za pomocą funkcji [hereNow](https://pubnub.com/docs/sdks/javascript/react)(). Jeśli całkowita zajętość jest mniejsza niż 2, gracz może pomyślnie zasubskrybować kanał lobby.

```js
// Check the number of people in the channel
this.pubnub.hereNow({
  channels: [this.lobbyChannel],
}).then((response) => {
    if(response.totalOccupancy < 2){
      this.pubnub.subscribe({
        channels: [this.lobbyChannel],
        withPresence: true
      });

      this.setState({
        piece: 'O', // Player O
      });

      this.pubnub.publish({
        message: {
          notRoomCreator: true,
        },
        channel: this.lobbyChannel
      });
    }
}).catch((error) => {
  console.log(error);
});
```

Gdy gracz zasubskrybuje kanał lobby, wartość stanu _elementu_ zostanie zmieniona na "O", a wiadomość zostanie opublikowana na tym kanale lobby. Wiadomość ta powiadamia gracza _X_, że do kanału dołączył inny gracz. Słuchacz wiadomości ustawiamy w funkcji **componentDidUpdate()**, do której przejdziemy wkrótce.

Jeśli całkowita zajętość jest większa niż 2, oznacza to, że gra jest w toku, a gracz próbujący dołączyć do kanału otrzyma odmowę dostępu. Poniższy kod znajduje się poniżej instrukcji if w funkcji **hereNow()**.

```js
// Below the if statement in hereNow()
else{
  // Game in progress
  Swal.fire({
    position: 'top',
    allowOutsideClick: false,
    title: 'Error',
    text: 'Game in progress. Try another room.',
    width: 275,
    padding: '0.7em',
    customClass: {
        heightAuto: false,
        title: 'title-class',
        popup: 'popup-class',
        confirmButton: 'button-class'
    }
  })
}
```

Przyjrzyjmy się teraz funkcji **componentDidUpdate()**.

### Rozpoczęcie gry

W **componentDidUpdate**() sprawdzamy, czy gracz jest podłączony do kanału, czyli sprawdzamy, czy _this.lobbyChannel_ nie ma wartości **null**. Jeśli nie ma wartości **null**, konfigurujemy nasłuchiwacza, który nasłuchuje wszystkich wiadomości przychodzących na kanał.

```js
componentDidUpdate() {
  // Check that the player is connected to a channel
  if(this.lobbyChannel != null){
    this.pubnub.getMessage(this.lobbyChannel, (msg) => {
      // Start the game once an opponent joins the channel
      if(msg.message.notRoomCreator){
        // Create a different channel for the game
        this.gameChannel = 'tictactoegame--' + this.roomId;

        this.pubnub.subscribe({
          channels: [this.gameChannel]
        });
      }
    });
  }
}
```

Sprawdzamy, czy otrzymana wiadomość to _msg.message.notRoomCreator_, która jest publikowana przez gracza dołączającego do kanału. Jeśli tak, tworzymy nowy kanał, "_tictactoegame-",_z _identyfikatorem pokoju_ dołączonym do ciągu znaków. Kanał gry jest używany do publikowania wszystkich ruchów wykonanych przez graczy, które zaktualizują ich plansze.

Wreszcie, po zasubskrybowaniu kanału gry, wartość stanu _isPlaying_ jest ustawiana na true. Spowoduje to zastąpienie komponentu lobby komponentem gry.

```js
 this.setState({
   isPlaying: true
 });

 // Close the modals if they are opened
 Swal.close();
}
```

Po wyświetleniu komponentu gry chcemy zamknąć wszystkie modale, jeśli zostały otwarte, z komponentu Lobby, wykonując _Swal.close()_.

Teraz, gdy mamy dwóch graczy podłączonych do unikalnego kanału gry, mogą zacząć grać w kółko i krzyżyk! W następnej sekcji zaimplementujemy interfejs użytkownika i logikę dla komponentu gry.

Tworzenie funkcji gry
---------------------

Pierwszą rzeczą, którą robimy w **Game.js**, jest skonfigurowanie **konstruktora bazowego**:

```js
// Game.js
import React from 'react';
import Board from './Board';
import Swal from "sweetalert2";

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(''), // 3x3 board
      xScore: 0,
      oScore: 0,
      whosTurn: this.props.myTurn // Player X goes first
    };

    this.turn = 'X';
    this.gameOver = false;
    this.counter = 0; // Game ends in a tie when counter is 9
  }

  render() {
    return ();
  }
 }
export default Game;
```

Dla obiektów stanu inicjalizujemy właściwość array _squares_, która służy do przechowywania pozycji gracza na planszy. Zostanie to wyjaśnione poniżej. Ustawiamy również wynik graczy na 0 i ustawiamy wartość _whosTurn_ na _myTurn_, która jest inicjowana na **true** dla gracza _X_ i **false** dla _gracza O_.

Wartości zmiennych _turn_ i _counter_ będą się zmieniać przez cały czas trwania gry. Na koniec gry wartość _gameOver_ jest ustawiana na **true**.

### Dodaj interfejs użytkownika

Następnie skonfigurujmy znaczniki dla komponentu Game wewnątrz metody **renderowania**.

```js
render() {
  let status;
  // Change to current player's turn
  status = `${this.state.whosTurn ? "Your turn" : "Opponent's turn"}`;

  return (
    <div className="game">
      <div className="board">
        <Board
            squares={this.state.squares}
            onClick={index => this.onMakeMove(index)}
          />
          <p className="status-info">{status}</p>
      </div>

      <div className="scores-container">
        <div>
          <p>Player X: {this.state.xScore} </p>
        </div>

        <div>
          <p>Player O: {this.state.oScore} </p>
        </div>
      </div>
    </div>
  );
}
```

Pokazujemy wartość _stanu_ w interfejsie użytkownika, aby poinformować graczy, czy to ich kolej na wykonanie ruchu, czy też kolej drugiego gracza. Wartość logiczna stanu _whosTurn_ jest aktualizowana za każdym razem, gdy wykonywany jest ruch. Reszta interfejsu użytkownika składa się z komponentu planszy i wyniku gracza.

### Dodaj logikę

Gdy gracz wykonuje ruch na planszy, wykonywane jest wywołanie **onMakeMove(****index)**, gdzie **index** jest pozycją, w której pion jest umieszczony na planszy. Plansza ma 3 rzędy i 3 kolumny, czyli łącznie 9 pól. Każde pole ma swoją unikalną wartość **indeksu**, zaczynając od wartości 0, a kończąc na wartości 8.

```js
onMakeMove = (index) =>{
  const squares = this.state.squares;

  // Check if the square is empty and if it's the player's turn to make a move
  if(!squares[index] && (this.turn === this.props.piece)){
    squares[index] = this.props.piece;

    this.setState({
      squares: squares,
      whosTurn: !this.state.whosTurn
    });

    // Other player's turn to make a move
    this.turn = (this.turn === 'X') ? 'O' : 'X';

    // Publish move to the channel
    this.props.pubnub.publish({
      message: {
        index: index,
        piece: this.props.piece,
        turn: this.turn
      },
      channel: this.props.gameChannel
    });

    // Check if there is a winner
    this.checkForWinner(squares)
  }
}
```

Po uzyskaniu stanu _pól_ tablicy, instrukcja warunkowa jest używana do sprawdzenia, czy pole, którego dotknął gracz, jest puste i czy nadeszła jego kolej na wykonanie ruchu. Jeśli jeden lub oba warunki nie są spełnione, pion gracza nie jest umieszczany na polu. W przeciwnym razie pion gracza jest dodawany do _pól_ tablicy w indeksie, na którym został umieszczony.

Na przykład, jeśli _gracz X_ wykona ruch w wierszu 0, kolumnie 2, a instrukcja warunkowa jest prawdziwa, wówczas _squares\[2\]_ będzie miało wartość "X".[![Example with the squares array](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png "Example with the squares array")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png)Następnie stan jest zmieniany, aby odzwierciedlić nowy stan gry, a _tura_ jest aktualizowana, aby drugi gracz mógł wykonać swój ruch. Aby plansza drugiego gracza została zaktualizowana o bieżące dane, publikujemy je na kanale gry. Wszystko to dzieje się w czasie rzeczywistym, więc obaj gracze natychmiast zobaczą aktualizację swoich plansz, gdy tylko zostanie wykonany prawidłowy ruch. Ostatnią rzeczą do zrobienia w tej metodzie jest wywołanie **checkForWinner(squares** ), aby sprawdzić, czy jest zwycięzca.

Zanim to zrobimy, przyjrzyjmy się metodzie **componentDidMount****()** , w której konfigurujemy odbiornik nowych wiadomości przychodzących do kanału gry.

```js
componentDidMount(){
  this.props.pubnub.getMessage(this.props.gameChannel, (msg) => {
    // Update other player's board
    if(msg.message.turn === this.props.piece){
      this.publishMove(msg.message.index, msg.message.piece);
    }
  });
}
```

Ponieważ obaj gracze są podłączeni do tego samego kanału gry, obaj otrzymają tę wiadomość. Wywoływana jest metoda **publishMove(index, piece** ), gdzie **index** to pozycja, na której umieszczony został pion, a **piece** to pion gracza, który wykonał ruch. Ta metoda aktualizuje planszę o bieżący ruch i sprawdza, czy jest zwycięzca. Aby gracz, który wykonał bieżący ruch, nie musiał ponownie wykonywać tego procesu, instrukcja _if_ sprawdza, czy pion gracza pasuje do wartości _turn_. Jeśli tak, jego plansza jest aktualizowana.

```js
// Opponent's move is published to the board
publishMove = (index, piece) => {
  const squares = this.state.squares;

  squares[index] = piece;
  this.turn = (squares[index] === 'X')? 'O' : 'X';

  this.setState({
    squares: squares,
    whosTurn: !this.state.whosTurn
  });

  this.checkForWinner(squares)
}
```

Logika aktualizacji planszy jest taka sama jak **onMakeMove()**. Przejdźmy teraz do **checkForWinner**().

```js
checkForWinner = (squares) => {
  // Possible winning combinations
  const possibleCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  // Iterate every combination to see if there is a match
  for (let i = 0; i < possibleCombinations.length; i += 1) {
    const [a, b, c] = possibleCombinations[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      this.announceWinner(squares[a]);
      return;
    }
  }
}
```

Wszystkie zwycięskie kombinacje znajdują się w podwójnej tablicy _possibleCombinations_, gdzie każda tablica jest możliwą kombinacją do wygrania gry. Każda tablica w _possibleCombinations_ jest porównywana z tablicą _kwadratów_. Jeśli istnieje zgodność, to jest zwycięzca. Prześledźmy to na przykładzie, aby uczynić to bardziej przejrzystym.

Powiedzmy, że gracz X wykonuje zwycięski ruch w wierszu 2 kolumnie 0. _Indeks_ tej pozycji wynosi 6. Plansza wygląda teraz następująco:

[![Example of a winning move](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png "Example of a winning move")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png)Zwycięska kombinacja dla gracza _X_ to \[2,4,6\]. Tablica _kwadratów_ została zaktualizowana do: \["O", "", "X", "O", "X", "", "X", "", ""\].

W pętli _for_, gdy _\[a,b,c\]_ mają wartości \[2,4,6\], instrukcja _if_ w pętli for ma wartość true, ponieważ \[2,4,6\] _mają_ tę samą wartość _X_. Wynik zwycięzcy musi zostać zaktualizowany, więc funkcja**announceWinner(** ) jest wywoływana w celu przyznania nagrody zwycięskiemu graczowi.

Jeśli gra zakończy się remisem, nie ma zwycięzcy w tej rundzie. Do sprawdzania remisów używamy licznika, który zwiększa się o jeden za każdym razem, gdy na planszy wykonywany jest ruch.

```js
// Below the for loop in checkForWinner()
// Check if the game ends in a draw
this.counter++;
// The board is filled up and there is no winner
if(this.counter === 9){
  this.gameOver = true;
  this.newRound(null);
}
```

Jeśli licznik osiągnie wartość 9, gra kończy się remisem, ponieważ gracz nie wykonał zwycięskiego ruchu na ostatnim polu planszy. W takim przypadku metoda **newRound(** ) jest wywoływana z argumentem **null**, ponieważ nie ma zwycięzcy.

Zanim przejdziemy do tej metody, wróćmy do funkcji**announceWinner**().

```js
// Update score for the winner
announceWinner = (winner) => {
  let pieces = {
    'X': this.state.xScore,
    'O': this.state.oScore
  }

  if(winner === 'X'){
    pieces['X'] += 1;
    this.setState({
      xScore: pieces['X']
    });
  }
  else{
    pieces['O'] += 1;
    this.setState({
      oScore: pieces['O']
    });
  }
  // End the game once there is a winner
  this.gameOver = true;
  this.newRound(winner);
}
```

Parametrem tej metody jest _zwycięzca_, czyli gracz, który wygrał grę. Sprawdzamy, czy zwycięzcą jest "X" lub "O" i zwiększamy wynik zwycięzcy o jeden punkt. Ponieważ gra jest zakończona, zmienna _gameOver_ jest ustawiana na true i wywoływana jest metoda **newRound** ().

### Rozpoczęcie nowej rundy

_Gracz X_ ma możliwość rozegrania kolejnej rundy lub zakończenia gry i powrotu do lobby.

[![Endgame modal for Player X](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png "Endgame modal for Player X")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png)

Drugi gracz musi poczekać, aż _gracz X_ zdecyduje, co zrobić.

[![Endgame modal for Player O](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png "Endgame modal for Player O")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png)

Gdy _gracz_ X zdecyduje, co zrobić, wiadomość zostanie opublikowana na kanale gry, aby poinformować o tym drugiego gracza. Następnie aktualizowany jest interfejs użytkownika.

```js
newRound = (winner) => {
  // Announce the winner or announce a tie game
  let title = (winner === null) ? 'Tie game!' : `Player ${winner} won!`;
  // Show this to Player O
  if((this.props.isRoomCreator === false) && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Waiting for a new round...',
      confirmButtonColor: 'rgb(208,33,41)',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
      } ,
    });
    this.turn = 'X'; // Set turn to X so Player O can't make a move
  }

  // Show this to Player X
  else if(this.props.isRoomCreator && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Continue Playing?',
      showCancelButton: true,
      confirmButtonColor: 'rgb(208,33,41)',
      cancelButtonColor: '#aaa',
      cancelButtonText: 'Nope',
      confirmButtonText: 'Yea!',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
          cancelButton: 'button-class'
      } ,
    }).then((result) => {
      // Start a new round
      if (result.value) {
        this.props.pubnub.publish({
          message: {
            reset: true
          },
          channel: this.props.gameChannel
        });
      }

      else{
        // End the game
        this.props.pubnub.publish({
          message: {
            endGame: true
          },
          channel: this.props.gameChannel
        });
      }
    })
  }
 }
```

Jeśli wiadomość zostanie _zresetowana_, wszystkie wartości stanu i zmienne, z wyjątkiem wyniku dla graczy, zostaną zresetowane do wartości początkowych. Wszelkie otwarte modale są zamykane i rozpoczyna się nowa runda dla obu graczy.

W przypadku komunikatu _endGame_ wszystkie modale są zamykane i wywoływana jest metoda **endGame()**. Metoda ta znajduje się w **App.js**.

```js
// Reset everything
endGame = () => {
  this.setState({
    piece: '',
    isPlaying: false,
    isRoomCreator: false,
    isDisabled: false,
    myTurn: false,
  });

  this.lobbyChannel = null;
  this.gameChannel = null;
  this.roomId = null;

  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

Wszystkie wartości stanu i zmienne są resetowane do wartości początkowych. Nazwy kanałów są resetowane do wartości null, ponieważ nowa nazwa jest generowana za każdym razem, gdy gracz tworzy pokój. Ponieważ nazwy kanałów nie będą już przydatne, gracze wypisują się zarówno z lobby, jak i kanału gry. Wartość _isPlaying_ jest resetowana do false, więc komponent gry zostanie zastąpiony komponentem lobby.

Ostatnią metodą do włączenia w **App.js** jest **componentWillUnmount()**, która wypisuje graczy z obu kanałów.

```js
componentWillUnmount() {
  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

To wszystko, co musimy zrobić, aby gra działała! Plik CSS dla gry można pobrać z [repozytorium](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Game.css). Teraz uruchommy grę.

Uruchom grę
-----------

Istnieje kilka małych kroków, które musimy wykonać przed uruchomieniem gry. Po pierwsze, musimy włączyć [funkcję Presence](https://pubnub.com/docs/sdks/javascript/react), ponieważ używamy jej do uzyskania liczby osób na kanale (użyliśmy _withPresence_ podczas subskrybowania kanału lobby). Przejdź do [panelu administratora PubNub](https://admin.pubnub.com/#/login) i kliknij swoją aplikację. Kliknij na **Keyset** i przewiń w dół do **Application add-ons**. Przełącz przełącznik **Presence** na **on**. Zachowaj domyślne wartości bez zmian.

[![Enable presence in PubNub Admin Dashboard](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png "Enable presence in PubNub Admin Dashboard")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png)

Aby zainstalować trzy zależności używane w aplikacji i uruchomić aplikację, możesz uruchomić skrypt **dependencies.sh**, który znajduje się w katalogu głównym aplikacji.

```js
# dependencies.sh
npm install --save pubnub pubnub-react
npm install --save shortid
npm install --save sweetalert2

npm start
```

W terminalu przejdź do katalogu głównego aplikacji i wpisz następujące polecenie, aby skrypt był wykonywalny:

```js
chmod +x dependencies.sh
```

Uruchom skrypt za pomocą tego polecenia:

```js
./dependencies.sh
```

Aplikacja otworzy się w http://localhost:3000 z wyświetlonym komponentem lobby.![Run the React app locally](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq9/fb4f1a520b9d68e1c283a61d5fa04653/run-the-app.png "Run the React app locally")Otwórz inną kartę, a najlepiej okno, i skopiuj i wklej http://localhost:3000. W jednym oknie utwórz kanał, klikając przycisk "Utwórz". Pojawi się okno modalne wyświetlające _identyfikator pokoju_. Skopiuj i wklej ten identyfikator. Przejdź do drugiego okna i kliknij przycisk "Dołącz". Gdy pojawi się okno modalne, wpisz identyfikator _pokoju_ w polu wprowadzania i naciśnij przycisk "OK".

[![Create and join the channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png "Create and join the channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png)

Gdy gracze zostaną połączeni, rozpocznie się gra. Okno użyte do utworzenia kanału wykona pierwszy ruch. Naciśnij dowolne pole na planszy i zobacz, jak pion **X** jest wyświetlany na planszy w czasie rzeczywistym dla obu okien. Jeśli spróbujesz nacisnąć inne pole na tej samej planszy, nic się nie stanie, ponieważ nie jest to już Twoja kolej na wykonanie ruchu. W drugim oknie naciśnij dowolne pole na planszy, a pion **O** **_zostanie_** umieszczony na tym polu.

[![Place the piece on the board](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png "Place the piece on the board")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png)

Kontynuuj grę do momentu wyłonienia zwycięzcy lub remisu. Następnie wyświetlony zostanie komunikat modalny ogłaszający zwycięzcę rundy lub informujący, że gra zakończyła się remisem. W tym samym oknie _gracz X_ będzie musiał zdecydować, czy kontynuować grę, czy ją zakończyć. Modal dla _gracza_ O powie mu, aby poczekał na nową rundę.

[![End of game modals](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png "End of game modals")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png)

Wszystko, z wyjątkiem wyniku, zostanie zresetowane, jeśli gracz _X_ będzie kontynuował grę. W przeciwnym razie obaj gracze są przenoszeni z powrotem do lobby, gdzie mogą tworzyć lub dołączać do nowych kanałów. Poniżej znajduje się demo gry:

**Masz sugestie lub pytania dotyczące treści tego wpisu? Skontaktuj się z nami pod adresem [devrel@pubnub.com](mailto:devrel@pubnub.com).**

#### Spis treści

[Koncepcje gry wieloosobowej w czasie](#h-0)[rzeczywistymPrzegląd aplikacji](#h-1)Ustaw[lobbyUtwórz kanałDołącz do](#h-3)[kanałuRozpocznij](#h-4)[gręZbuduj](#h-5)[funkcje gryDodaj](#h-6)[UIAdodaj](#h-7)[logikęRozpocznij](#h-8)[nową rundęUruchom](#h-9)[grę](#h-10)

Jak PubNub może ci pomóc?
=========================

Ten artykuł został pierwotnie opublikowany na [PubNub.com](https://www.pubnub.com/blog/browser-based-multiplayer-tic-tac-toe-game-in-react/)

Nasza platforma pomaga programistom tworzyć, dostarczać i zarządzać interaktywnością w czasie rzeczywistym dla aplikacji internetowych, aplikacji mobilnych i urządzeń IoT.

Podstawą naszej platformy jest największa w branży i najbardziej skalowalna sieć komunikacyjna w czasie rzeczywistym. Dzięki ponad 15 punktom obecności na całym świecie obsługującym 800 milionów aktywnych użytkowników miesięcznie i niezawodności na poziomie 99,999%, nigdy nie będziesz musiał martwić się o przestoje, limity współbieżności lub jakiekolwiek opóźnienia spowodowane skokami ruchu.

Poznaj PubNub
-------------

Sprawdź [Live Tour](https://www.pubnub.com/tour/introduction/), aby zrozumieć podstawowe koncepcje każdej aplikacji opartej na PubNub w mniej niż 5 minut.

Rozpocznij konfigurację
-----------------------

Załóż [konto](https://admin.pubnub.com/signup/) PubNub, aby uzyskać natychmiastowy i bezpłatny dostęp do kluczy PubNub.

Rozpocznij
----------

[Dokumenty](https://www.pubnub.com/docs) PubNub pozwolą Ci rozpocząć pracę, niezależnie od przypadku użycia lub [zestawu SDK](https://www.pubnub.com/docs).