Как создать игру для двух игроков с помощью Python и Vue

1656548649 kak sozdat igru dlya dvuh igrokov s pomoshhyu python i

автор Нео Игадаро

В этом учебнике мы создадим игру в крестики-нолики в реальном времени с помощью каналов Python и Pusher. Вот демонстрация того, как игра будет выглядеть и вести себя после создания:

MuzX8tcbRETbqqNijym55kvnzPcPcsOqZbJp

Вам пригодятся Python 3+, virtualenv и Flask, установленные на вашей машине. Появление ПК и Интернета изменило определение термина развлечения и средств, с помощью которых их можно получить. Хотя раньше для игры в игры требовалась консоль или какое-нибудь специальное оборудование, в современном мире технологий игры доступны только одним кликом.

Эта игра для нескольких игроков позволит игроку подключиться, используя свое желаемое имя пользователя (или создать случайное имя пользователя, если игрок не соединится с именем пользователя) и выбрать играть с другим игроком из списка других онлайн-игроков.

Сама игра придерживается общепринятых принципов популярной игры в крестики-нолики. Функция «онлайн-игроков» работает с помощью каналов присутствия Pusher, а обновление в реальном времени о перемещении игрока через несколько окон осуществляется с помощью приватных каналов Pusher. Выходной код этого учебника доступен здесь GitHub.

Давайте начнем.

Предпосылки

Для продолжения необходимы базовые знания Python, Flask, JavaScript (синтаксис ES6) и Vue. На вашем компьютере также необходимо установить следующее:

  1. Python (v3.x)
  2. Virtualenv
  3. Колба

Virtualenv отлично подходит для создания изолированных сред Python, поэтому мы можем устанавливать зависимости в изолированной среде, не загрязняя наш глобальный каталог пакетов.

Настройка среды

Мы создадим папку проекта и активируем в ней виртуальную среду:

$ mkdir python-pusher-mutiplayer-game
    $ cd python-pusher-mutiplayer-game
    $ virtualenv .venv
    $ source .venv/bin/activate # Linux based systems
    $ \path\to\env\Scripts\activate # Windows users

Мы установим Flask с помощью этой команды:

$ pip install flask

Настройка Pusher

Чтобы интегрировать Pusher в многопользовательскую игру, нам нужно создать программу Pusher channels на информационной панели Pusher. Если у вас нет учетной записи Pusher, перейдите на веб-сайт Pusher и создайте его.

После создания аккаунта создайте новую программу каналов и включите события клиента на информационной панели программы. Для включения событий клиента нажмите на Настройка программы и прокрутите вниз страницы, а затем выберите опцию Включить клиентские события, и обновить Настройка программы.

Создание бэкенд-сервера

Вернувшись в каталог проекта, давайте установим библиотеку Python Pusher с помощью этой команды:

$ pip install pusher

Мы создадим новый файл и вызовем его app.py, здесь мы напишем весь код серверного сервера Flask. Мы также создадим папку и вызовем ее templatesэта папка сохраняет файлы разметки для этого приложения.

Давайте напишем код, чтобы зарегистрировать конечные точки для игры и обслуживать представления, откройте файл app.py файл и вставьте следующий код:

// File: ./app.py
    from flask import Flask, render_template, request, jsonify, make_response, json
    from pusher import pusher
    app = Flask(__name__)
    pusher = pusher_client = pusher.Pusher(
      app_id='PUSHER_APP_ID',
      key='PUSHER_APP_KEY',
      secret="PUSHER_APP_SECRET",
      cluster="PUSHER_APP_CLUSTER",
      ssl=True
    )
    name=""
    @app.route('/')
    def index():
      return render_template('index.html')
    @app.route('/play')
    def play():
      global name
      name = request.args.get('username')
      return render_template('play.html')
    @app.route("/pusher/auth", methods=['POST'])
    def pusher_authentication():
      auth = pusher.authenticate(
        channel=request.form['channel_name'],
        socket_id=request.form['socket_id'],
        custom_data={
          u'user_id': name,
          u'user_info': {
            u'role': u'player'
          }
        }
      )
      return json.dumps(auth)
    if __name__ == '__main__':
        app.run(host="0.0.0.0", port=5000, debug=True)
    name=""

Замените PUSHER_APP_* клавиши со значениями в информационной панели Pusher.

В коде выше мы определили три конечных точки, вот что они делают:

  • / — отображает первую страницу, которая просит игрока подключиться к имени пользователя.
  • /play — отображает вид игры.
  • /pusher/auth — аутентифицирует присутствие и приватные каналы Pusher для подключенных игроков.

Создание интерфейса

В templates папку, мы создадим два файла:

  1. index.html
  2. play.html

The index.html файл отобразит страницу подключения, поэтому откройте файл templates/index.html файл и вставьте следующий код:

<!-- File: ./templates/index.html -->
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="description" content="">
            <meta name="author" content="Neo Ighodaro">
            <title>TIC-TAC-TOE</title>
            <link rel="stylesheet" href="
            <style>
                  :root {
                    --input-padding-x: .75rem;
                    --input-padding-y: .75rem;
                  }
                  html,
                  body, body > div {
                    height: 100%;
                  }
                  body > div {
                    display: -ms-flexbox;
                    display: flex;
                    -ms-flex-align: center;
                    align-items: center;
                    padding-top: 40px;
                    padding-bottom: 40px;
                    background-color: #f5f5f5;
                  }
                  .form-signin {
                    width: 100%;
                    max-width: 420px;
                    padding: 15px;
                    margin: auto;
                  }
                  .form-label-group {
                    position: relative;
                    margin-bottom: 1rem;
                  }
                  .form-label-group > input,
                  .form-label-group > label {
                    padding: var(--input-padding-y) var(--input-padding-x);
                  }
                  .form-label-group > label {
                    position: absolute;
                    top: 0;
                    left: 0;
                    display: block;
                    width: 100%;
                    margin-bottom: 0; /* Override default `<label>` margin */
                    line-height: 1.5;
                    color: #495057;
                    cursor: text; /* Match the input under the label */
                    border: 1px solid transparent;
                    border-radius: .25rem;
                    transition: all .1s ease-in-out;
                  }
                  .form-label-group input::-webkit-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input:-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-moz-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::placeholder {
                    color: transparent;
                  }
                  .form-label-group input:not(:placeholder-shown) {
                    padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
                    padding-bottom: calc(var(--input-padding-y) / 3);
                  }
                  .form-label-group input:not(:placeholder-shown) ~ label {
                    padding-top: calc(var(--input-padding-y) / 3);
                    padding-bottom: calc(var(--input-padding-y) / 3);
                    font-size: 12px;
                    color: #777;
                  }
            </style>
          </head>
          <body>
            <div id="app">
              <form class="form-signin">
                <div class="text-center mb-4">
                  <img class="mb-4" src=" alt="" width="72" height="72">
                  <h1 class="h3 mb-3 font-weight-normal">TIC-TAC-TOE</h1>
                  <p>PUT IN YOUR DETAILS TO PLAY</p>
                </div>
                <div class="form-label-group">
                    <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus="">
                      <label for="inputUsername">Username</label>
                </div>
                <div class="form-label-group">
                  <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required>
                    <label for="inputEmail">Email address</label>
                </div>
                <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button>
                <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p>
              </form>
            </div>
            <script src="
            <script>
            var app = new Vue({
              el: '#app',
              methods: {
                login: function () {
                  let username = this.$refs.username.value
                  let email = this.$refs.email.value
                  window.location.replace(`/play?username=${username}&email=${email}`);
                }
              }
            })
            </script>
        </body>
    </html>

Когда игрок посещает страницу подключения и вводит имя пользователя и адрес электронной почты, окно браузера будет перенаправлено на просмотр игры.

Давайте напишем разметку для просмотра игры. Откройте play.html файл и вставьте следующий код:

<!-- file: ./templates/play.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
      <link rel="stylesheet" href="
      <title>TIC-TAC-TOE</title>
    </head>
    <body>
      <div id="app" class="container-fluid">
        <div class="container-fluid clearfix mb-3 shadow">
          <img class="float-left my-3" src=" height="62px" width="62px"
          />
          <div class="float-right w-25 py-3">
            <img class="my-3 mx-3 rounded-circle border" src="
              height="62px" width="62px" />
            <p class="d-inline"> {% raw %} {{ username }} {% endraw %} </p>
          </div>
        </div>
        <div class="row mx-5" style="height: 50vh">
          <div class="col-8 h-50 align-self-center">
            <div class="row border rounded invisible h-50 w-75 m-auto" style="font-size: 3.6rem" ref="gameboard" @click="playerAction">
              <div class="h-100 pr-2 col border border-dark" data-id="1" ref="1"></div>
              <div class="col pr-2 border border-dark" data-id="2" ref="2"></div>
              <div class="col pr-2 border border-dark" data-id="3" ref="3"></div>
              <div class="w-100"></div>
              <div class="h-100 pr-2 col border border-dark" data-id="4" ref="4"></div>
              <div class="col pr-2 border border-dark" data-id="5" ref="5"></div>
              <div class="col pr-2 border border-dark" data-id="6" ref="6"></div>
              <div class="w-100"></div>
              <div class="h-100 pr-2 col border border-dark" data-id="7" ref="7"></div>
              <div class="col pr-2 border border-dark" data-id="8" ref="8"></div>
              <div class="col pr-2 border border-dark" data-id="9" ref="9"></div>
            </div>
          </div>
          <div class="col-4 pl-3">
            <div class="row h-100">
              <div class="col border h-75 text-center" style="background: rgb(114, 230, 147);">
                <p class="my-3"> {% raw %} {{ players }} {% endraw %} online player(s) </p>
                <hr/>
                <li class="m-auto py-3 text-dark" style="cursor: pointer;" v-for="member in connectedPlayers" @click="choosePlayer">
                  {% raw %} {{ member }} {% endraw %}
                </li>
              </div>
              <div class="w-100"></div>
              <div class="col text-center py-3 border h-25" style="background: #b6c0ca; font-size: 1em; font-weight: bold">
                {% raw %} {{ status }} {% endraw %}
              </div>
            </div>
          </div>
        </div>
      </div>
      <script src="
      <script src="
      <script>
      </script>
    </body>
    </html>

Вышеприведенный код определяет макет просмотра игры, но не содержит никаких интерактивных функций или функций реального времени. В разделе скриптов перед закрытием body мы включили библиотеки Vue и Pusher, потому что они необходимы для работы игры.

Давайте включим JavaScript, который будет управлять всем процессом игры и определять его логику.

В этом же файле добавьте следующий код между файлами script тег, который находится непосредственно перед закрытием body тег:

var app = new Vue({
      el: '#app',
      data: {
        username: '',
        players: 0,
        connectedPlayers: [],
        status: '',
        pusher: new Pusher('PUSHER_APP_KEY', {
          authEndpoint: '/pusher/auth',
          cluster: 'PUSHER_APP_CLUSTER',
          encrypted: true
        }),
        otherPlayerName: '',
        mychannel: {},
        otherPlayerChannel: {},
        firstPlayer: 0,
        turn: 0,
        boxes: [0, 0, 0, 0, 0, 0, 0, 0, 0]
      },
      created () {
        let url = new URL(window.location.href);
        let name = url.searchParams.get("username");
        if (name) {
          this.username = name
          this.subscribe();
          this.listeners();
        } else {
          this.username = this.generateRandomName();
          location.assign("/play?username=" + this.username);
        }
      },
      methods: {
        // We will add methods here
      }
    });

Замените PUSHER_APP_* ключи с клавишами на панели приборов Pusher.

Выше мы создаем новый экземпляр Vue и нацеливаемся на #app селектор. Мы определяем все значения по умолчанию в dataобъект, а затем в create() функция, которая вызывается автоматически, когда создается компонент Vue, мы проверяем пользователя и назначаем ему имя пользователя, если оно было предоставлено.

Также мы совершаем звонки на subscribe и listeners методы Давайте определим тех, кто находится внутри methods объект. Внутри methods объект, вставьте следующие функции:

// [...]
    subscribe: function () {
      let channel = this.pusher.subscribe('presence-channel');
      this.myChannel = this.pusher.subscribe('private-' + this.username)
      channel.bind('pusher:subscription_succeeded', (player) => {
        this.players = player.count - 1
        player.each((player) => {
          if (player.id != this.username)
            this.connectedPlayers.push(player.id)
        });
      });
      channel.bind('pusher:member_added', (player) => {
        this.players++;
        this.connectedPlayers.push(player.id)
      });
      channel.bind('pusher:member_removed', (player) => {
        this.players--;
        var index = this.connectedPlayers.indexOf(player.id);
        if (index > -1) {
          this.connectedPlayers.splice(index, 1)
        }
      });
    },
    listeners: function () {
      this.pusher.bind('client-' + this.username, (message) => {
        if (confirm('Do you want to start a game of Tic Tac Toe with ' + message)) {
          this.otherPlayerName = message
          this.otherPlayerChannel = this.pusher.subscribe('private-' + this.otherPlayerName)
          this.otherPlayerChannel.bind('pusher:subscription_succeeded', () => {
            this.otherPlayerChannel.trigger('client-game-started', this.username)
          })
          this.startGame(message)
        } else {
          this.otherPlayerChannel = this.pusher.subscribe('private-' + message)
          this.otherPlayerChannel.bind('pusher:subscription_succeeded', () => {
            this.otherPlayerChannel.trigger('client-game-declined', "")
          })
          this.gameDeclined()
        }
      }),
      this.myChannel.bind('client-game-started', (message) => {
        this.status = "Game started with " + message
        this.$refs.gameboard.classList.remove('invisible');
        this.firstPlayer = 1;
        this.turn = 1;
      })
      this.myChannel.bind('client-game-declined', () => {
        this.status = "Game declined"
      })
      this.myChannel.bind('client-new-move', (position) => {
        this.$refs[position].innerText = this.firstPlayer ? 'O' : 'X'
      })
      this.myChannel.bind('client-your-turn', () => {
        this.turn = 1;
      })
      this.myChannel.bind('client-box-update', (update) => {
        this.boxes = update;
      })
      this.myChannel.bind('client-you-lost', () => {
        this.gameLost();
      })
    },
    // [...]

В subscribe методом мы подписываем на наш канал присутствия Pusher, а затем подписываемся на частный канал для текущего пользователя. В listeners методом мы регистрируем слушателей для всех событий, которые мы ожидаем на приватном канале, на который мы подписались.

Далее мы добавим другие вспомогательные методы к нашему классу методов. Внутри класса методов добавьте следующие функции внизу после listeners метод:

// Generates a random string we use as a name for a guest user
    generateRandomName: function () {
      let text="";
      let possible="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
      for (var i = 0; i < 6; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
      }
      return text;
    },
    // Lets you choose a player to play as.
    choosePlayer: function (e) {
      this.otherPlayerName = e.target.innerText
      this.otherPlayerChannel = this.pusher.subscribe('private-' + this.otherPlayerName)
      this.otherPlayerChannel.bind('pusher:subscription_succeeded', () => {
        this.otherPlayerChannel.trigger('client-' + this.otherPlayerName, this.username)
      });
    },
    // Begins the game
    startGame: function (name) {
      this.status = "Game started with " + name
      this.$refs.gameboard.classList.remove('invisible');
    },
    // User declined to play
    gameDeclined: function () {
      this.status = "Game declined"
    },
    // Game has ended with current user winning
    gameWon: function () {
      this.status = "You WON!"
      this.$refs.gameboard.classList.add('invisible');
      this.restartGame()
    },
    // Game has ended with current user losing
    gameLost: function () {
      this.turn = 1;
      this.boxes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
      this.status = "You LOST!"
      this.$refs.gameboard.classList.add('invisible');
      this.restartGame()
    },
    // Restarts a game
    restartGame: function () {
      for (i = 1; i < 10; i++) {
        this.$refs[i].innerText = ""
      }
      this.$refs.gameboard.classList.remove('invisible');
    },
    // Checks tiles to see if the tiles passed are a match
    compare: function () {
      for (var i = 1; i < arguments.length; i++) {
        if (arguments[i] === 0 || arguments[i] !== arguments[i - 1]) {
          return false
        }
      }
      return true;
    },
    // Checks the tiles and returns true if theres a winning play
    theresAMatch: function () {
      return this.compare(this.boxes[0], this.boxes[1], this.boxes[2]) ||
        this.compare(this.boxes[3], this.boxes[4], this.boxes[5]) ||
        this.compare(this.boxes[6], this.boxes[7], this.boxes[8]) ||
        this.compare(this.boxes[0], this.boxes[3], this.boxes[6]) ||
        this.compare(this.boxes[1], this.boxes[4], this.boxes[7]) ||
        this.compare(this.boxes[2], this.boxes[5], this.boxes[8]) ||
        this.compare(this.boxes[2], this.boxes[4], this.boxes[6]) ||
        this.compare(this.boxes[0], this.boxes[4], this.boxes[8])
    },
    // Checks to see if the play was a winning play
    playerAction: function (e) {
      let index = e.target.dataset.id - 1
      let tile = this.firstPlayer ? 'X' : 'O'
      if (this.turn && this.boxes[index] == 0) {
        this.turn = 0
        this.boxes[index] = tile
        e.target.innerText = tile
        this.otherPlayerChannel.trigger('client-your-turn', "")
        this.otherPlayerChannel.trigger('client-box-update', this.boxes)
        this.otherPlayerChannel.trigger('client-new-move', e.target.dataset.id)
        if (this.theresAMatch()) {
          this.gameWon()
          this.boxes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
          this.otherPlayerChannel.trigger('client-you-lost', '')
        }
      }
    },

Выше мы добавили несколько вспомогательных методов, которые нужны игре для надлежащей работы, и перед каждым методом мы добавили комментарий, чтобы показать, что делает метод.

Давайте сейчас протестируем игру.

Тестирование игры

Мы можем протестировать игру, выполнив эту команду:

$ flask run

Теперь, если мы посетим localhost:5000, мы должны увидеть страницу подключения и протестировать игру:

Vmj8BM420J2JpEiLvfPO7qrLxfYiPHIGuwI6

Вывод

В этом учебнике мы узнали, как использовать Pusher SDK для создания многопользовательской онлайн игры на базе сервера Python.

Исходный код данного руководства доступен на GitHub

Эта публикация впервые появилась в блоге Pusher

Добавить комментарий

Ваш адрес email не будет опубликован.