Здесь вы узнаете, как реализовать простую игру на квантовом компьютере. Для этой цели мы воспользуемся классической игрой Quantum Battleship (квантовый «Морской бой»), поставляемой вместе с учебником по QISKit на Python. В первой части рассматривается механика игры, включая:
• использование кубитов для представления позиций кораблей на игровом поле;
• расчет возможного ущерба с применением квантовой программы для запуска на локальном, удаленном моделирующем или реальном квантовом устройстве;
• способ поворота вокруг оси X одного кубита с помощью частичного квантового вентиля НЕ.
И мы не остановимся на этом. Во второй части главы выйдем на следующий уровень. Игра будет значительно модифицирована за счет реализации облачной версии Quantum Battleship со следующими характеристиками.
• Интерфейс пользователя с браузерной поддержкой и интерактивными игровыми полями для размещения кораблей и бомб. Механика игры остается прежней.
• Сценарий на основе CGI, использующий веб-сервер Apache для получения игровых событий и отправки их в квантовую программу.
• Модифицированная версия исходной квантовой программы для выполнения частичных поворотов при помощи вентиля НЕ на кубитах для расчета ущерба, нанесенного кораблям. Бо'льшая часть исходного кода остается неизменной.
Вы узнаете, как исходный код можно сделать модульным, чтобы повторно применять для другой версии Quantum Battleship в облаке. Приступим!
В этом разделе рассмотрим игру под названием Quantum Battleship, распространяемую вместе с руководством по QISKit. В программе используются пять кубитов для представления игрового поля, где каждый игрок размещает три корабля. Затем каждого игрока просят разместить бомбу в позиции 0–5. Ущерб для каждого корабля рассчитывается с помощью квантовой программы, которая применяет однобитовый двухимпульсный вентиль: U3(θ, φ, λ). Он называется частичным вентилем НЕ и выполняет поворот вокруг осей X, Y или Z на θ, φ или λ радиан.
В данном случае ущерб, нанесенный кораблю, рассчитывается путем выполнения серии частичных поворотов вокруг оси X (θ) с учетом количества бомб для этой позиции. Если урон для позиции или корабля превышает 95 %, корабль уничтожен, а после разгрома всего флота одного из игроков объявляется победитель и игра заканчивается. Это самый обычный «Морской бой», в который мы все играли в детстве, но с квантовым компьютером или моделирующим устройством на заднем плане.
Примечание
Игру написал Джеймс Вуттон из Базельского университета. Она внесена в учебник по QISKit на Python. Модифицированная версия оригинального кода Вуттона доступна в исходниках для этой книги по адресу Workspace\Ch06\battleship\BattleShip.py (за исключением ненужного текста с причудливым форматированием).
Запустим программу и рассмотрим механику игры.
Запустите на выполнение программу BattleShip.py из исходников к этой книге, как описано далее.
1. Для CentOS 6 или 7 или любой ОС, подобной Fedora, активируйте виртуальную среду Python. Это необходимо, только если у вас есть несколько версий Python, например 2.7 и 3.6. Помните, что вы должны использовать версию 3.5 или более позднюю. Инструкции по настройке виртуальной среды Python приведены в главе 3.
2. Скопируйте сценарий из Workspace\Ch06\battleship\BattleShip.py и файл конфигурации Qconfig.py из исходников книги в свое рабочее пространство и запустите его на выполнение, как показано в следующем фрагменте кода:
# Активация виртуальной среды Python3 на $HOME/qiskit/qiskit
$ source $HOME/qiskit/qiskit/bin/activate
$ python BattleShip.py
############### Quantum Battle Ship ##################
Do you want to play on the real device? (y/n) n
Рассмотрим работу программы.
В листинге 6.1 демонстрируется инициализация сценария. Она начинается с выполнения основных задач Python.
• Загружаются системные библиотеки sys и QuantumProgram, необходимые для всех операций QISKit.
• Проверяется то, что вы используете Python 3.5 или более позднюю версию.
• Программа спрашивает, хотите ли вы задействовать моделирующее устройство или настоящий квантовый компьютер. Затем устанавливается количество запусков на выполнение, по умолчанию 1024.
Листинг 6.1. Инициализация сценария
#################################
# Quantum Battleship из учебника @
# https://github.com/QISKit/qiskit-tutorial
################################## import sys
# проверка версии PYTHON; поддерживаются только версии > 3.5
if sys.version_info < (3,5):
raise Exception('Please use Python version 3.5 or greater.')
from qiskit import QuantumProgram
import Qconfig
import getpass, random, numpy, math
## 1. Выбор серверного ПО: моделирующее устройство IBM
(ibmqx_qasm_simulator) или реальный чип ibmqx2
d = input("Do you want to play on the real device? (y/n)\n").upper()
if (d=="Y"):
device = 'ibmqx2'
else:
device = 'ibmqx_qasm_simulator'
# Обратите внимание, что устройством может быть 'ibmqx_qasm_simulator',
# 'ibmqx2' или 'local_ qasm_simulator'
# Установка количества запусков
shots = 1024
Совет
Чтобы запустить квантовую программу на реальном устройстве, вы должны поместить файл конфигурации (Qconfig.py) в то же место, что и основной сценарий. Конфигурация содержит требуемый токен API и конечную точку IBM Q Experience:
APItoken = 'YOUR API TOKEN'
config = {
'url': 'https://quantumexperience.ng.bluemix.net/api',
}
Теперь расставим несколько кораблей на игровом поле.
В программе используется элементарный текстовый интерфейс для всего пользовательского ввода. В листинге 6.2 показана логика ввода кораблей для каждого игрока. Нажмите Enter, чтобы начать, и введите позиции трех кораблей для каждого игрока (нумерация позиций с нуля).
• Сценарий может сам выбрать случайные позиции либо игрок должен ввести позиции для трех кораблей.
• Позиции хранятся в двумерном списке shipPos, где shipPos[0] содержит позиции первого игрока, а shipPos[1] — второго. Помните, что для одного игрока доступно только три корабля.
Листинг 6.2. Размещение кораблей на игровом поле
####### 2. Размещение кораблей на игровом поле игроком
randPlace = input("> Press Enter to start placing ships…\n").upper()
# В переменной ship[X][Y] будет храниться позиция Y-го корабля игрока X+1
shipPos = [ [-1]*3 for _ in range(2)]
# Проход в цикле по обоим игрокам и трем кораблям для каждого
for player in [0,1]:
# Если выбор производится не игроком, а случайным образом
if ((randPlace=="r")|(randPlace=="R")):
randPos = random.sample(range(5), 3)
for ship in [0,1,2]:
shipPos[player][ship] = randPos[ship]
else:
for ship in [0,1,2]:
# Запрос позиции для каждого корабля
choosing = True
while (choosing):
# Получить ввод игрока
position = getpass.getpass("Player " + str(player+1)
+ ", choose a position for ship " + str(ship+1) +
" (0-4)\ n" )
# Проверка корректности ввода и запрос на повторный ввод
if position.isdigit(): # Ответ должен быть целым числом
position = int(position)
# и находиться в диапазоне от 0 до 4
if (position in [0,1,2,3,4]) and (not position in
shipPos[player]):
shipPos[player][ship] = position
choosing = False
print ("\n")
elif position in shipPos[player]:
print("\nYou already have a ship there. Try
again.\n")
else:
print("\nThat's not a valid position. Try again.\n")
else:
print("\nThat's not a valid position. Try again.\n")
Далее показан стандартный вывод, очень простой, но пока этого достаточно:
Do you want to play on the real device? (y/n) n
Player 1, choose a position for ship 1 (0, 1, 2, 3 or 4)
0
Player 1, choose a position for ship 2 (0, 1, 2, 3 or 4)
1
Player 1, choose a position for ship 3 (0, 1, 2, 3 or 4)
2
Player 2, choose a position for ship 1 (0, 1, 2, 3 or 4)
0
Player 2, choose a position for ship 2 (0, 1, 2, 3 or 4)
1
Player 2, choose a position for ship 3 (0, 1, 2, 3 or 4)
2
Интересные процессы происходят в основном цикле. Рассмотрим его.
В основном цикле выполняются следующие задания.
• Обоих игроков просят поместить по одной бомбе в позицию [0–4]. Количество бомб хранится в двумерном списке из пяти элементов (два игрока, пять счетчиков бомб). Обратите внимание, что игрок может бомбить одну и ту же позицию несколько раз; таким образом, если первый игрок наносит удар по нулевой позиции дважды, то bombs=[[2,0,0,0,0],[0,0,0,0,0]].
• Создается QuantumProgram для хранения пяти кубитов (по одному на каждую позицию на доске) и пяти классических регистров, чтобы хранить результаты измерений.
• Если позиция бомбы совпадает с позицией корабля противника (из списка shipPos), ущерб рассчитывается выполнением одного поворота вокруг оси X по количеству бомб с использованием частичного однокубитного вентиля НЕ: gridScript.u3(1/(ship+1)*math.pi,0.0,0.0,q[position]). Обратите внимание, что эффективность бомбы также зависит от того, какой корабль бомбят (0, 1, 2).
• Для завершения схемы на кубите выполняется измерение для позиции, а результат сохраняется в соответствующем классическом регистре: gridScript.measure(q[position],c[position]).
• Затем программа выполняется на целевом устройстве, а результаты сохраняются в двумерном списке grid. Например, если нулевая позиция первого игрока разбомблена, то grid=[[1,0,0,0,0],[0,0,0,0,0]]. Далее показано, как это происходит:
results = Q_program.execute(["gridScript"], backend=device,
shots=shots)
grid[player] = results.get_counts("gridScript")
• Результаты проверяются на наличие ошибок. Если все верно, то процент ущерба рассчитывается в промежутке [0, 1], когда в списке grid содержится значение 1 для этой позиции. Проценты сохраняются в двумерном списке повреждений. Таким образом, ущерб [[0,95,0,0,0,0],[0,0,0,0,0]] указывает на то, что корабль первого игрока на нулевой позиции был уничтожен.
• Наконец, игрокам выводятся результаты. Процесс повторяется до тех пор, пока все корабли не будут уничтожены и не будет объявлен победитель (листинг 6.3).
Листинг 6.3. Основной цикл игры «Морской бой»
? 100 %
|\ /|
| \ / |
| \ / |
| ? |
| / \ |
| / \ |
|/ \|
? ?
########### 3. Основной цикл
# Каждая итерация начинается с опроса игроков о том, где
# на игровом поле противника должна быть размещена бомба
# Квантовый компьютер подсчитывает эффект от нанесенных ударов
# и предоставляет результаты
# Игра продолжается, пока все корабли одного из игроков
# не будут уничтожены
game = True
# В переменной bombs[X][Y] хранится количество раз, когда позиция Y
# была разбомблена игроком X+1
bomb = [ [0]*5 for _ in range(2)] # все значения инициализированы нулями
# В переменной grid[player] будут храниться результаты для каждого игрока
grid = [{},{}]
while (game):
input("> Press Enter to place some bombs…\n")
# Обоих игроков спрашивают, где они хотят разместить бомбы
for player in range(2):
print("\n\nIt's now Player " + str(player+1) + "'s turn.\n")
# Вопрос повторяется, пока не будет получено корректное значение
choosing = True
while (choosing):
# Получить ввод пользователя
position = input("Choose a position to bomb (0, 1, 2, 3
or 4)\n")
# Проверка корректности ввода
# Запрос на повторный ввод, если предыдущий был некорректным
if position.isdigit(): # ответ должен быть целым числом
position = int(position)
if position in range(5):
bomb[player][position] = bomb[player][position] + 1
choosing = False
print ("\n")
else:
print("\nThat's not a valid position. Try again.\n")
else:
print("\nThat's not a valid position. Try again.\n")
# Теперь мы создаем и запускаем на выполнение квантовую программу
# для каждого игрока
for player in range(2):
if device=='ibmqx2':
print("\nUsing a quantum computer for Player " + str(player+1)
+ "'s ships.\n")
else:
print("\nUsing the simulator for Player " + str(player+1) +
"'s ships.\n")
# Теперь настройка квантовой программы (QASM) для моделирования
# сетки для данного игрока
Q_program = QuantumProgram()
# Установка APIToken и url-адреса API
Q_program.set_api(Qconfig.APItoken, Qconfig.config["url"])
# Объявление регистров пяти кубитов
q = Q_program.create_quantum_register("q", 5)
# Объявление регистров пяти классических битов для хранения
# результатов измерений
c = Q_program.create_classical_register("c", 5)
# Создание схемы
gridScript = Q_program.create_circuit("gridScript", [q], [c])
# Добавление бомб (противника)
for position in range(5):
# Добавить столько бомб, сколько было помещено в данную позицию
for n in range( bomb[(player+1)%2][position] ):
# Эффективность бомбы
# (квантовой операции, которую мы применим)
# зависит от вида корабля
for ship in [0,1,2]:
if ( position == shipPos[player][ship] ):
frac = 1/(ship+1)
# Добавление этой части вентиля НЕ в QASM
gridScript.u3(frac * math.pi, 0.0, 0.0, q[position])
# Наконец, выполнение измерений
for position in range(5):
gridScript.measure(q[position], c[position])
# чтобы увидеть, какие действия должен выполнить квантовый
# компьютер, мы можем вывести на печать файл c QASM
# Эта строка обычно закомментирована
#print( Q_program.get_qasm("gridScript") )
# Скомпилировать и запустить на выполнение QASM
results = Q_program.execute(["gridScript"], backend=device,
shots=shots)
# Извлечение данных
grid[player] = results.get_counts("gridScript")
# Данные при желании можно проверить
# Эти строки обычно закомментированы
#print( grid[0] )
#print( grid[1] )
# Если один из запусков потерпел неудачу, сообщить об этом игрокам
# и начать раунд заново
if ( ( 'Error' in grid[0].values() ) or ( 'Error' in grid[1].
Values() ) ):
print("\nThe process timed out. Try this round again.\n")
else:
# Рассмотрим ущерб во всех кубитах (даже там, где нет кораблей)
# Для каждого кубита каждого игрока будет сохранено значение
# вероятности 1
damage = [ [0]*5 for _ in range(2)]
# Для этого мы пройдем в цикле по всем пяти битовым строкам
# каждого из игроков
for player in range(2):
for bitString in grid[player].keys():
# А затем по всем позициям
for position in range(5):
# Если в строке для данной позиции находится значение 1,
# то распределение вероятности добавляется в ущерб
# Помните, что бит нулевой позиции крайний справа
# и соответствует bitString[4]
if (bitString[4-position]=="1"):
damage[player][position] += grid[player]
[bitString]/shots
# Вывод результатов игрокам
for player in [0,1]:
input("\nPress Enter to see the results for Player
" + str(player+1) + "'s ships…\n")
# Отчет о существенном ущербе по кубитам для кораблей;
# в идеале это будет ненулевой ущерб,
# таким образом, мы выбираем пороговое значение 5 %
display = [" ? "]*5
# Перебрать в цикле кубиты для кораблей
for position in shipPos[player]:
# Если ущерб довольно велик, отобразить его
if ( damage[player][position] > 0.1 ):
if (damage[player][position]>0.9):
display[position] = "100 %"
else:
display[position] = str(int( 100*damage[player]
[position] )) + "% "
print("Here is the percentage damage for ships that have been
bombed.\n")
print(display[ 4 ] + " " + display[ 0 ])
print(" |\ /|")
print(" | \ / |")
print(" | \ / |")
print(" | " + display[ 2 ] + " |")
print(" | / \ |")
print(" | / \ |")
print(" |/ \|")
print(display[ 3 ] + " " + display[ 1 ])
print("\n")
print("Ships with 95 % damage or more have been destroyed\n")
print("\n")
# Если все корабли одного игрока уничтожены, игра окончена
# В идеале это предполагает 100%-ный ущерб, но мы остановимся
# на 90 % опять же по причине шума
if (damage[player][ shipPos[player][0] ]>.9) and
(damage[player][ shipPos[player][1] ]>.9)
and (damage[player][ shipPos[player][2] ]>.9):
print ("***All Player " + str(player+1) + "'s ships have
been destroyed!***\n\n")
game = False
if (game is False):
print("")
print("=======GAME OVER=======")
print("")
Обратите внимание, что если ущерб превысит 90 %, то корабль будет помечен как уничтоженный. В листинге 6.4 показаны результаты одного игрового взаимодействия.
Листинг 6.4. Стандартный вывод для одного игрового взаимодействия
> Press Enter to place some bombs…
It's now Player 1's turn.
Choose a position to bomb (0, 1, 2, 3 or 4)
0
It's now Player 2's turn.
Choose a position to bomb (0, 1, 2, 3 or 4)
0
We'll now get the simulator to see what happens to Player 1's ships.
We'll now get the simulator to see what happens to Player 2's ships.
Press Enter to see the results for Player 1's ships…
Here is the percentage damage for ships that have been bombed.
? 100 %
|\ /|
| \ / |
| \ / |
| ? |
| / \ |
| / \ |
|/ \|
? ?
Ships with 95% damage or more have been destroyed
Press Enter to see the results for Player 2's ships…
Here is the percentage damage for ships that have been bombed.
? 100 %
|\ /|
| \ / |
| \ / |
| ? |
| / \ |
| / \ |
|/ \|
? ?
Ships with 95 % damage or more have been destroyed
Таким образом, основной цикл продолжается до тех пор, пока не будет объявлен победитель. Итак, вы узнали, как можно реализовать простую игру, чтобы использовать квантовый компьютер для выполнения несложных расчетов нанесенных повреждений путем поворотов кубита вокруг оси X. Эта версия довольно примитивна, но интересна. Однако мы можем сделать лучше. В следующем разделе усовершенствуем дизайн игры.
Действительно, здорово иметь возможность играть в «Морской бой» на квантовом компьютере через простой текстовый интерфейс, но гораздо круче играть в эту игру в браузере в облаке. В этом разделе мы модифицируем Quantum Battleship и улучшим ее дизайн, в чем игра, несомненно, нуждается (рис. 6.1).
Рис. 6.1. Структура Quantum Battleship в облаке
Идея заключается в следующем.
• Откажитесь от скучного текстового интерфейса в пользу HTML-страницы, которую можно развернуть в облаке.
• Используйте общедоступный шлюзовой интерфейс (Common Gateway Interface, CGI) веб-сервера Apache для развертывания квантовой логики в сценарии.
• Дайте игроку возможность выбрать, на чем выполнять расчеты: на локальном или дистанционном моделирующем устройстве либо на реальном квантовом компьютере.
Реализуем это в серии упражнений, описанных в следующих разделах.
Один из основных принципов объектно-ориентированного программирования таков: никогда не смешивайте представление (интерфейс пользователя) и бизнес-логику. Спроектированные таким образом компоненты можно скомпилировать и повторно использовать где угодно. В случае с Battleship нам нужно выполнить следующее.
• Удалить или закомментировать первый раздел сценария, в котором считывается положение кораблей для каждого игрока (хороший кусок кода), стараясь не затронуть какие-либо структуры данных или переменные.
• Удалить или закомментировать все операторы вывода на печать и ввода с клавиатуры.
• Удалить основной цикл игры, в котором постоянно указывается позиция для бомбардировки. Сценарий должен завершиться после того, как он использует данные из HTTP-запроса. В нем не может быть бесконечных циклов, иначе запрос зависнет.
• Добавить в сценарий поддержку CGI в Python, чтобы из HTTP-запроса можно было прочитать данные, включая:
• позиции кораблей для каждого игрока;
• позиции и количество бомб для каждого игрока;
• устройство для запуска квантовых вычислений.
Сценарий должен возвращать отчет о повреждениях (желательно в формате JSON) через HTTP-ответ, чтобы браузер отображал его в JavaScript.
Обратите внимание, что мы будем повторно использовать бо'льшую часть кода: структуры данных, локальные переменные и квантовую логику. В этом заключается причина комментирования всех операторов для ввода данных и вывода на печать. Решение этого (как и всех остальных) упражнения приведено в конце данного раздела.
Создайте графический интерфейс пользователя для HTML по аналогии с текстовым, и используйте AJAX для асинхронной отправки запросов в CGI-сценарий. Верните результаты нанесенного ущерба и, наконец, обновите игровые поля. Улучшенный дизайн можно увидеть на рис. 6.2.
Рис. 6.2. Интерфейс пользователя для новой версии Quantum Battleship
• В HTML-файле будет четыре игровых поля, в каждом 3 × 3 ячейки. Верхние игровые поля используются для того, чтобы разместить три корабля в пяти местоположениях, соответствующих кубитам. Их реализуем в виде флажков (<INPUTTYPE="checkbox">). Мы воспользуемся CSS для замены кнопки-переключателя изображением, поэтому вместо нее при щелчке на ячейке будет переключаться изображение корабля.
• Нижние игровые поля позволят игрокам размещать бомбы в пяти местах при помощи все того же CSS, что и в предыдущем абзаце, но они будут реализованы как <INPUTTYPE="radio">, так что в каждом местоположении может быть размещено несколько бомб.
• Несмотря на то что размер игрового поля 3 × 3, для пользовательского ввода доступны только пять местоположений, соответствующих каждому кубиту в квантовой программе.
• Для каждого местоположения корабля будут отображаться номер кубита и возвращаемый серверным ПО процент ущерба с цветной подсветкой.
• Механика игры точно такая же, как и в текстовой версии.
• Оба игрока размещают на игровых полях по три корабля, а затем каждый по очереди размещает бомбу и нажимает кнопку Submit (Отправить). CGI-сценарий Python получит запрос через AJAX, запустит квантовую программу, созданную в упражнении 6.1, и вернет результат для ущерба, который будет отображен в JavaScript.
• Обратите внимание, что все игровое состояние, массивы, переменные и другие данные хранятся в клиентском HTML, поэтому мы должны использовать AJAX для асинхронной отправки запроса, иначе данные будут потеряны при каждой отправке игроком. Обновлений страницы не будет.
На рис. 6.3 показаны нижние игровые поля 3 × 3 ячейки, отображающие номера кубитов, количество щелчков на бомбе и изображения переключателей, выводимые с использованием CSS. Когда каждый игрок размещает три корабля и выбирает место для бомбардировки, он нажимает кнопку Submit (Отправить) для отправки запроса на сервер AJAX. Кнопка сброса также доступна, чтобы игру можно было в любой момент перезапустить.
Рис. 6.3. Игровые поля с бомбами в Quantum Battleship
Учтите, что все состояние сохраняется в клиенте (браузере). Никакие данные не будут храниться в сценарии Python, поскольку HTTP является протоколом запроса-ответа без сохранения состояния. Это означает, что, когда запрос получен, программа выполняется веб-сервером, ответ печатается в буфере вывода запроса и программа завершается. Как и для предыдущего упражнения, решение находится в конце этого раздела.
Теперь, когда все на месте, пришло время для развертывания на веб-сервере. Я буду использовать сервер Apache под названием HTTPD в CentOS 6, но он должен работать для любой версии CentOS, Fedora или Red Hat (возможно, для любого текущего дистрибутива Linux с HTTPD). Имейте в виду, что каждый вариант имеет свои особенности при настройке системного ПО. Например, CentOS фокусируется на стабильности и безопасности, что доставляет мне много головной боли при настройке HTTPD и Python.
В этом разделе представлен CGI-скрипт Python, который получает HTTPD-запрос от браузера и отвечает строкой в формате JSON, содержащей отчет о нанесенном ущербе и другую информацию. Первая часть программы осталась практически неизменной, за исключением того, что теперь ввод нужно проанализировать из HTTP-запроса с применением библиотеки cgi в Python (листинг 6.5).
Листинг 6.5. Инициализация модульной версии Quantum Battleship
import sys
from qiskit import QuantumProgram
import Qconfig
import getpass, random, numpy, math
import cgi
import cgitb
# Разрешите относительные зависимости, если вы клонируете QISKit
# из репозитория Git и используете как глобальный
sys.path.append('../../qiskit-sdk-py/')
# Отладка
cgitb.enable(display=0, logdir=".")
# В переменной ship[X][Y] будет храниться позиция Y-го корабля игрока X + 1
# все переменные инициализированы -1| (значение для невозможной позиции)
shipPos = [ [-1]*3 for _ in range(2)]
# В переменной bombs[X][Y] будет храниться количество раз, когда позицию Y
# бомбил игрок X+1
bomb = [ [0]*5 for _ in range(2)] # все значения равны 0
В листинге 6.5 показана первая часть сценария. В шестой и седьмой строках импортируются библиотеки Python: cgi и cgitb (CGI Toolbox), которые используются для чтения данных из HTTP-запросов и отладки CGI-программ соответственно.
Примечание
Строки в следующем фрагменте кода активируют специальный обработчик исключений, который будет отображать подробные отчеты в браузере в случае возникновения ошибки.
import cgitb
cgitb.enable()
Имейте в виду, что при возникновении ошибки мы не можем показать внутреннюю информацию программы, так как клиент ожидает ответа в формате JSON. Вместо этого мы должны сохранять отчеты об ошибках в текущем рабочем каталоге при помощи кода, аналогичного следующему:
cgitb.enable (display = 0, logdir = ".")
Этот код избавит от проблем во время разработки, поскольку любое исключение будет сохранено в аккуратном HTML-документе в текущем рабочем каталоге.
Формат документа показан в разделе «Устранение ошибок» данной главы.
В листинге 6.5 также показаны структуры данных, используемые для хранения игрового состояния. Они те же самые, что и в старой версии:
• shipPos — двумерный список, в котором хранятся позиции для трех кораблей на игрока, инициализированные –1. Таким образом, shipPos=[[-1,-1,-1],[-1,-1,-1]];
• bomb — двумерный список, в котором хранится количество бомб на позицию для каждого игрока. Инициализирован нулями: bomb=[[0,0,0,0,0],[0,0,0,0,0]].
Обратите внимание, что одну и ту же позицию можно бомбить несколько раз, поэтому необходимо хранить количество. Этот список будет использоваться для расчета ущерба, причиненного кораблю.
Затем сценарий считывает игровые данные из HTTP-запроса (листинг 6.6).
Листинг 6.6. Чтение данных из HTTP-запроса
# CGI-анализ HTTP-запроса
form = cgi.FieldStorage()
ships1 = form["ships1"].value
ships2 = form["ships2"].value
bombs1 = form["bombs1"].value
bombs2 = form["bombs2"].value
# 'local_qasm_simulator', 'ibmqx_qasm_simulator'
device = str(form["device"].value)
shipPos[0] = list(map(int, ships1.split(","))) # [0,1,2]
shipPos[1] = list(map(int, ships2.split(","))) # [0,1,2]
bomb[0] = list(map(int, bombs1.split(",")))
bomb[1] = list(map(int, bombs2.split(",")))
stdout = "Ship Pos: " + str(shipPos) + " Bomb counts: " + str(bomb) + "<br>"
Чтобы прочитать данные из HTTP-запроса, используйте form=cgi.FieldStorage(). Этот вызов CGI возвращает словарь или хеш-карту пар «ключ — значение», с помощью которой извлекаются параметры строки запроса. В данном конкретном случае ожидаются следующие значения:
• ships1 — трехэлементный массив данных в формате JSON с позициями корабля первого игрока;
• ships2 — трехэлементный массив данных в формате JSON с позициями корабля второго игрока;
• bombs1 — пятиэлементный массив данных в формате JSON со счетчиками бомб первого игрока;
• bombs2 — пятиэлементный массив данных в формате JSON со счетчиками бомб второго игрока;
• device — устройство, на котором будет выполняться квантовая программа. Это может быть:
• local_qasm_simulator — локальное моделирующее устройство, упакованное вместе с QISKit;
• ibmq_qasm_simulator — удаленное моделирующее устройство, предоставляемое IBM;
• ibmqx2 — пятикубитный квантовый процессор, предоставляемый IBM Q Experience.
Самое замечательное в Python — это то, что данные в формате JSON, предоставляемые HTTP-запросом, в два счета могут быть отображены в поддерживаемые им коллекции:
shipPos[0] = list(map(int, ships1.split(",")))
bomb[0] = list(map(int, bombs1.split(",")))
Примечание
В Python системный вызов split(SEPARATOR) используется для создания списка элементов типа String. Но нам нужен список целых чисел. Для его получения мы применяем системный вызов map(DATA-TYPE, LIST). Обратите внимание, что в Python 3 вызов map возвращает хеш-таблицу (словарь), поэтому мы должны использовать системный вызов list для преобразования в нужный нам список целых чисел. Это здорово, потому что сценарий может повторно задействовать старые структуры данных и получится сохранять большую часть квантовой логики нетронутой.
Последняя строка листинга 6.6 — это просто строковый буфер стандартного вывода, который будет возвращен браузеру для отладки. Наконец, в листинге 6.7 показана внутренняя логика сценария, которая по большей части остается нетронутой.
Листинг 6.7. Основной раздел квантового сценария
# В переменной grid[player] будут храниться результаты для поля каждого
# игрока
grid = [{},{}]
# Теперь мы создаем и запускаем на выполнение квантовые программы,
# которые реализуют на поле каждого игрока следующее
for player in range(2):
# Настройка квантовой программы (QASM) для моделирования сетки
# данного игрока
Q_program = QuantumProgram()
Q_program.set_api(Qconfig.APItoken, Qconfig.config["url"])
# Объявление регистра из пяти кубитов
q = Q_program.create_quantum_register("q", 5)
# Объявление регистра из пяти классических битов для хранения
# результатов измерений
c = Q_program.create_classical_register("c", 5)
# Создание схемы
gridScript = Q_program.create_circuit("gridScript", [q], [c])
# Добавление бомб (установленных противником)
for position in range(5):
# Добавляется столько бомб, сколько было помещено в данную позицию
for n in range( bomb[(player+1)%2][position] ):
# Эффективность бомбы
# (квантовой операции, которая будет применена)
# зависит от типа корабля
for ship in [0,1,2]:
if ( position == shipPos[player][ship] ):
frac = 1/(ship+1)
# Добавление этой части вентиля НЕ в QASM
gridScript.u3(frac * math.pi, 0.0, 0.0, q[position])
# Наконец, выполнить измерения
for position in range(5):
gridScript.measure(q[position], c[position])
# Чтобы увидеть, какие действия должен выполнить квантовый
# компьютер, мы можем вывести на печать файл c QASM
# Эта строка обычно закомментирована
#print( Q_program.get_qasm("gridScript") )
# Скомпилировать и запустить на выполнение QASM
results = Q_program.execute(["gridScript"], backend=device,
shots=shots)
# Извлечение данных
grid[player] = results.get_counts("gridScript")
# Если один из запусков был неудачным, сообщить об этом игрокам
# и начать раунд заново
if ( ( 'Error' in grid[0].values() ) or ( 'Error' in grid[1].
Values() ) ):
stdout += "The process timed out. Try this round again.<br>"
else:
# Рассмотрим ущерб во всех кубитах (даже там, где нет кораблей)
damage = [ [0]*5 for _ in range(2)]
# Для этого мы пройдем в цикле по всем пяти битовым строкам
# каждого из игроков
for player in range(2):
for bitString in grid[player].keys():
# а затем по всем позициям
for position in range(5):
# Если в строке для данной позиции находится значение 1,
# то распределение вероятности добавляется в ущерб
# Помните, что бит нулевой позиции — крайний справа
# и соответствует bitString[4]
if (bitString[4-position]=="1"):
damage[player][position] += grid[player][bitString]/
shots
stdout += "Damage: " + str(damage) + "<br>"
В основной раздел первоначального сценария было внесено несколько небольших изменений.
• Все операторы вывода на печать были удалены. Вместо этого используется стандартный выходной строковый буфер для возврата информации клиенту. Это сделано потому, что при выводе в Python информация будет выгружаться непосредственно в HTTP-ответ, что испортит формат JSON, который мы должны вернуть обратно (JavaScript ожидает правильный JSON от AJAX). Обратите внимание, что это необязательный, но полезный шаг, предназначенный для возврата отладочной информации клиенту. В общем, вы можете обойти его, просто закомментировав все операторы вывода на печать (конечно, если произойдет ошибка, трудно будет разобраться, что пошло не так).
• Все операторы пользовательского ввода (считывание положения бомбы, нажатие клавиши Enter для продолжения и др.) были удалены. Помните, что позиции кораблей и счетчики бомб отображаются в коллекции из HTTP-запроса.
• Первоначальный сценарий использует бесконечный цикл while для чтения позиций бомбы. Этот цикл удален. Если этого не сделать, сценарий будет работать вечно и зависнет HTTP-запрос.
Наконец, сценарий возвращает документ в формате JSON в браузер для обновления интерфейса пользователя (листинг 6.8).
Листинг 6.8. Отправка ответа в браузер
# Ответ
print ("Content-type: application/json\n\n")
print ("{\"status\": 200, \"message\": \"" + stdout + "\", \"damage\":" + str(damage) + "}")
Для пересылки ответа в браузер с помощью CGI в Python просто отправьте на печать стандартный HTTP-ответ, чтобы получить стандартный вывод, то есть один или несколько заголовков HTTP, за которыми следуют два символа перевода строки и тело ответа. Например, чтобы отправить документ об ущербе в формате JSON, мы используем фрагмент:
Content-type: application/json
{ "status" : 200, "message": "Device ibmqx_qasm_simulator", "damage":
[[0.5, 0, 0, 0, 0], [0, 0.9, 0, 0, 0]]}
Предыдущий документ в формате JSON указывает на повреждение qubit(0) первого игрока и qubit(1) второго игрока. Этот документ будет проанализирован с помощью кода AJAX браузера, и данные на экране будут обновлены.
Примечание
Код для этого упражнения доступен в исходниках книги в файле Workspace\Ch06\battleship\cgi-bin\qbattleship.py.
На веб-странице используется таблица 2 × 2 ячейки для отображения внутренних таблиц размерности 3 × 3, которые представляют собой игровые поля для размещения кораблей и бомб, как показано на рис. 6.4 и в листинге 6.9.
Листинг 6.9. Код HTML для рис. 6.4
<form id="frm1">
Device
<select id="device" name="device">
<option value="local_qasm_simulator">Local Simulator</option>
<option value="ibmqx_qasm_simulator">IBM Simulator</option>
<option value="ibmqx2">ibmqx2</option>
</select>
Place 3 ships per player, place a bomb & click submit.
<table>
<tr>
<td>
<div><h3>Player 1</h3></div>
Рис. 6.4. Интерфейс пользователя Quantum Battleship
<script type="text/javascript"> table(1, 's')</script>
</td>
<td>
<div><h3>Player 2</h3></div>
<script type="text/javascript"> table(2, 's')</script>
</td>
</tr>
<tr>
<td>
<div><h3>Player 1 Bombs</h3></div>
<script type="text/javascript"> table(1, 'b')</script>
</td>
<td>
<div><h3>Player 2 Bombs</h3></div>
<script type="text/javascript"> table(2, 'b')</script>
</td>
</tr>
</table>
</form>
Рисование игровых полей 3 × 3 ячейки происходит динамически при помощи системного вызова document.write() (листинг 6.10).
Листинг 6.10. Динамическое рисование таблиц с помощью document.write()
// type: 's' (ship) = checkbox, 'b' (bomb) = radio
function table (player, type) {
var d = document;
var html = '<table border="1">\n';
var qubit = 0;
for ( var i = 0 ; i < 3 ; i ++) {
html += '<tr>';
for ( var j = 0 ; j < 3 ; j ++) {
if ( (i + j) % 2 == 0) {
var id = 'p' + player + type + qubit++;
// checkbox = ship, radio = bomb
var itype = type == 's' ? 'checkbox' : 'radio';
var extra = type == 'b' ? ' onclick="cell_click_
bomb(this)"'
: ' onclick="return cell_click_ship(this)"';
// <TD> SHIP-INDEX DAMAGE IMAGE </TD>
html += '<td>' + (qubit — 1)
+ ' <span id="' + type + player + (qubit -1 ) + '">
</span>'
+ '<input id="' + id + '" name="' + id + '" type="' +
itype + '"' + extra + '>'
+ '<label for="' + id + '" class="ship">
</label></td>'
} else {
html += '<td> </td>';
}
} html += '</tr>\n';
} html += '</table>'; d.write(html);
}
В табл. 6.1 приведены основные особенности интерфейса пользователя.
Таблица 6.1. Интерфейс пользователя облачной версии Battleship, приемы и хитрости
Мы скрываем флажки и переключатели при помощи таблиц стилей. Селекторы в строках 1 и 2 используют псевдокласс отрицания, чтобы скрыть правило от старых браузеров. Строки с 3-й по 5-ю задают ширину, поля и отступы для точного позиционирования альтернативной графики. В строке 6 устанавливается нулевое значение для непрозрачности, что делает стандартный интерфейс пользователя невидимым | input[type=checkbox]:not(old), input[type=radio ]:not(old){ width : 104px; margin : 0; padding : 0; opacity : 0; } |
Каждая ячейка в таблице кораблей отображает: • номер кубита; • элемент span для отображения процента нанесенного ущерба; • <INPUT type = "checkbox">, модифицированный для использования изображения 100 × 100 пикселов вместо обычного элемента управления
| input[type=checkbox]:not(old) + label { display : inline-block; margin-left : –104px; padding-left : 104px; background : url(‘img/ship.png’) no-repeat 0 0; line-height : 100px; } Мы размещаем метку и выводим изображение с неустановленным флажком. В строке 2 для метки устанавливается отображение в виде элемента inline-block, что позволяет в строке 6 задать высоту в соответствии с размерами альтернативной графики и центрировать текст по вертикали. В строке 3 используется отрицательное значение для поля (margin), чтобы покрыть пространство, в котором будет отображаться стандартный интерфейс пользователя, а в строке 4 применяется отступ, чтобы восстановить правильное положение текста метки. Используемое здесь значение 104 пиксела равно ширине изображения плюс некоторый дополнительный отступ, чтобы текст метки не был расположен слишком близко к изображению. В строке 5 показано изображение с неустановленным флажком в поле перед текстом метки |
Каждая ячейка в таблице бомб содержит: • номер кубита; •элемент span для отображения количества бомб в данной позиции; •<INPUT type="radio"> модифицированный для использования изображения 100 × 100 пикселов вместо обычного элемента управления
| Стиль, который применяется для форматирования бомб, приведен далее: input[type=radio ]:not(old) + label{ display : inline-block; margin-left : –104px; padding-left : 104x; background : url('img/bomb.png') no-repeat 0 0; line-height : 100px; } Затем отображаются выбранные картинки, для которых установлены флажки и переключатели: input[type=checkbox]:not(old):checked + label{ background-position : 0 –100px; } input[type=radio]:not(old):checked + label{ background-position : 0 –100px; } Поскольку мы объединили изображения для различных состояний в одно, приведенные ранее правила изменяют положение фона, чтобы показать соответствующее изображение |
Прекрасные библиотеки jQuery, Bootstrap и Bootstrap-Growl используются для отображения сообщений и отладки информации в консоли JS: <script type="text/javascript" src="js/log.js"></script> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/bootstrap.js"></script> <script type="text/javascript" src="js/bootstrap-growl.js"> </script> <script type="text/javascript" src="js/notify.js"></script> | Чтобы HTML-код стал красивым, применяется типичная для проектирования GUI библиотека Bootstrap. Сообщения отображаются на экране с помощью библиотеки Bootstrap-Growl из JS: notify('Bomb ready. Click Submit', info);
|
Поскольку HTTP — это протокол без сохранения состояния, все структуры данных и логика проверки должны быть перенесены на сторону клиента. Например:
• игрокам нельзя разрешать размещать на доске более трех кораблей;
• запрещено менять судно после установки бомбы;
• бомбы нельзя устанавливать до того, как все игроки разместят свои корабли;
• для отслеживания щелчков кнопкой мыши используется глобальный массив счетчиков бомб varBOMBS=[[0,0,0,0,0],[0,0,0,0,0]]. Он соответствует аналогу в Python bomb=[[0]*5for_inrange(2)].
Эти правила можно применить, добавив обратный вызов при щелчке на ячейке корабля или бомбы (листинг 6.11).
Листинг 6.11. Установление правил игры с помощью обратных вызовов в исходном коде книги в index.html
// Вызывается при щелчке на ячейке корабля
function cell_click_ship ( obj ) {
var id = obj.id;
var player = parseInt(id.charAt(1));
var qubit = parseInt(id.charAt(3));
var json = countShipsBombs();
LOGD('Cell Clicked ' + id + ' Counts:' + JSON.stringify(json));
if ( json.ships[0] > 3 || json.ships[1] > 3) {
return error('All Players must place only 3 ships.');
}
// Запрет на изменение кораблей после установки бомб
if ( json.bombs[0] > 0 || json.bombs[1] > 0 ) {
return error('No ship changes after bombs are placed.');
} return true;
}
// Вызывается при щелчке на ячейке бомбы
function cell_click_bomb ( obj ) {
var id = obj.id; // Для бомб: p[PLAYER]b[QUBIT]
var player = parseInt(id.charAt(1));
var qubit = parseInt(id.charAt(3));
// Проверка: { 'ships': [s1, s2], 'bombs': [b1, b2]}
var json = countShipsBombs();
LOGD('Bomb Clicked ' + id + ' Counts:' + JSON.stringify(json));
if ( json.ships[0] < 3 || json.ships[1] < 3) {
$('#' + id).attr('checked', false);
return error('All Players must place 3 ships first.');
}
if ( mustSubmit) {
return error('Bomb in place already. Click Submit.');
}
// Проверка очереди игрока. Ошибки?
var dif = (json.bombs[player – 1] + 1) – json.bombs[ 1 – (player – 1)];
if ( dif >= 2 ) {
if ( BOMBS[player – 1 ][qubit] < 1 ) {
$('#' + id).attr('checked', false);
}
return error("Not your turn. It's player " + ((1-(player-1)) + 1) );
}
// Подсчет бомб
BOMBS[player – 1 ][qubit]++;
// Присвоить счетчики: d[PLAYER][QUBIT]
$('#b' + player + qubit).html("(" + BOMBS[player – 1 ][qubit] + ")");
// Бомбы на месте, нажать Подтвердить
notify('Bomb ready. Click Submit', 'info');
mustSubmit = true;
}
function error (msg) {
notify(msg, 'danger');
return false
}
Теперь данные могут быть отправлены на сервер.
Каждый запрос должен отправляться на веб-сервер асинхронно с использованием AJAX. Кроме того, строка запроса должна иметь определенный формат.
Для конечной точки http://localhost/~centos/battleship/cgi-bin/qiskit-driver.sh мы предполагаем, что:
• имя пользователя — centos;
• код был развернут в домашней папке пользователя $HOME/centos/public_html/battleship;
• Python 3 необходимо активировать с помощью сценария-обертки qiskit-driver.sh. Это требуется только в том случае, если на хосте имеется несколько версий Python (см. далее раздел «Запуск нескольких версий Python»).
Строка запроса должна содержать следующие значения:
• ship1 — разделенный запятыми список из трех позиций для кораблей первого игрока;
• ship2 — разделенный запятыми список из трех позиций для кораблей второго игрока;
• bombs1 — разделенный запятыми список из пяти бомб для первого игрока;
• bombs2 — разделенный запятыми список из пяти бомб для второго игрока;
• device — квантовое устройство, например local_qasm_simulator, ibmq_qasm_simulator или ibmqx2.
Вот пример полного запроса AJAX для запуска на удаленном моделирующем устройстве:
http://localhost/~centos/battleship/cgi-bin/qiskit-driver.sh?ships1=0,1,2&s hips2=0,1,2&bombs1=0,1,0,0,0&bombs2=0,0,0,0,0&device=ibmqx_qasm_simulator
Когда игрок нажимает кнопку Submit (Отправить), позиции кораблей ships1 и ships2, а также счетчики бомб bombs1 и bombs2 собираются из дерева DOM и глобального массива BOMBS. Конечная точка и строка запроса определены, и HTTP-запрос GET отправляется через AJAX (листинг 6.12).
Листинг 6.12. Отправка данных на сервер в файле index.html
function submit() {
var frm = $('#frm1');
var url = "cgi-bin/qiskit-driver.sh";
// Формат данных: ships1=0,1,2&ships2=0,1,2&bombs1=0,1,0,0,0
// &bombs2=0,0,0,0,0
// Для кораблей указаны позиции по игрокам,
// Для бомб указано количество для их позиций по игрокам
// Корабли: три корабля на игрока, бомбы: пять счетчиков для позиций
var data = ";
var s1 = ";
var s2 = ";
for ( var i = 0 ; i < 5 ; i++) {
if ( $('#p1s' + i).prop('checked') ) s1 += ',' + i;
if ( $('#p2s' + i).prop('checked') ) s2 += ',' + i;
}
// Удаление первой точки
if (s1.length > 0) s1 = s1.substring(1);
if (s2.length > 0) s2 = s2.substring(1);
// Строка запроса
data = 'ships1=' + s1 + '&ships2=' + s2
+ '&bombs1=' + BOMBS[0].join(',') + '&bombs2=' + BOMBS[1].join(',')
+ '&device=' + $('#device').val();
LOGD('Url:' + url + ' data=' + data);
// https://api.jquery.com/jquery.get/
$.get( url, data)
.done(function (json) {
handleResponse (json);
})
.fail(function() {
LOGD( "error" );
notify('Internal Server Error. Check logs.', 'danger');
})
}
Если что-то пойдет не так, уведомление об ошибке будет отображаться на экране, в противном случае ожидаемый ответ в формате JSON будет отправлен обработчику. Посмотрим, как это происходит.
Задача обработчика ответа состоит в том, чтобы использовать ответ сервера и обновлять счетчики нанесенного ущерба, отображать сообщения об ошибках, если таковые имеются, или повторять этот процесс, пока не будет объявлен победитель. Процесс показан в листинге 6.13, но сначала рассмотрим имеющий большое значение формат ответа JSON:
{"status":200,"message":"OK","damage":[[0.475,0,0,0.70,0],
[0.786,0.90,0,0,0.]]}
Самым важным ключом является damage. Он содержит двумерный массив, представляющий для каждого игрока позиции, в которых корабль поврежден. Значение ущерба находится в диапазоне от 0 до 1. Эти данные используются обработчиком ответа для обновления интерфейса пользователя.
Листинг 6.13. Обработчик ответа в файле index.html
function handleResponse (json) {
LOGD("Got: " + JSON.stringify(json))
var damage = json.damage;
var d1 = damage[0]; // ущерб для P1
var d2 = damage[1]; // ущерб для P2
for ( var i = 0 ; i < 5 ; i++) {
var pct1 = (d1[i] * 100).toFixed(1);
var pct2 = (d2[i] * 100).toFixed(1);
var s1, c1, s2, c2;
if ( pct1 < 90 ) {
s1 = '[' + pct1 + '%]';
c1 = 'cyan';
}
else {
s1 = 'SUNK';
c1 = 'red';
notify('Player 1 Ship ' + i + ' sunk.', 'warning');
}
if ( pct2 < 90 ) {
s2 = '[' + pct2 + '%]';
c2 = 'cyan';
}
else {
s2 = 'SUNK';
c2 = 'red';
notify('Player 2 Ship ' + i + ' sunk.', 'warning');
}
//LOGD(i + ' s1=' + s1 + ' s2=' + s2 + ' d1=' + d1[i] +
// ' d2=' + d2[i]);
$('#s1' + i).html(s1).css('background-color', c1);
$('#s2' + i).html(s2).css('background-color', c2);
}
// Результат игры: суммарный ущерб sum > 2.85 (0.95 * 3) = loss
// https://www.w3schools.com/jsref/jsref_reduce.asp
// array.reduce(function(total, currentValue, currentIndex, arr),
// initialValue)
var s1 = d1.reduce(function(total, currentValue, currentIndex, arr)
{ return total + currentValue}, 0);
var s2 = d2.reduce(function(total, currentValue, currentIndex, arr)
{ return total + currentValue}, 0);
var winner = 0;
if ( s1 > 2.85) winner = 2;
if ( s2 > 2.85) winner = 1;
LOGD ("Results Damage sums s1:" + s1 + " s2:" + s2);
if ( winner != 0) {
notify ('** G.A.M.E O.V.E.R Player ' + winner + ' wins **',
'success'); gameover = true;
}
// Разрешить отправку
$("#btnSubmit").prop("disabled", false);
}
Обработчик извлекает массив для ущерба и проходит в цикле по всем позициям, переводя ущерб для каждого игрока в проценты.
Чтобы более эффектно продемонстрировать нанесенный урон, используется цветная подсветка. Для каждого затонувшего корабля на экран выводятся сообщения (рис. 6.5).
Рис. 6.5. Цветная подсветка поля для отображения ущерба
Если суммарный ущерб превышает предельное значение 90 % для всех кораблей игрока, объявляется победитель и игра заканчивается. Чтобы начать новую игру, нажмите кнопку Reset (Сбросить).
Чтобы запустить игру заново, мы просто снимаем все флажки и переключатели и обнуляем глобальный массив BOMBS (листинг 6.14).
Листинг 6.14. Перезагрузка игры в исходном коде книги в index.html
// Перезапуск игры вызывается при нажатии кнопки перезапуска
function reset_click () {
if ( ! confirm("Are you sure?")) {
return;
}
gameover = false;
for ( var i = 0 ; i < 5 ; i++) {
$('#p1s' + i).attr('checked', false);
$('#p2s' + i).attr('checked', false);
$('#p1b' + i).attr('checked', false);
$('#p2b' + i).attr('checked', false);
// элементы span с информацией
$('#s1' + i).html(");
$('#s2' + i).html(");
$('#b1' + i).html(");
$('#b2' + i).html(");
BOMBS[0][i] = 0;
BOMBS[1][i] = 0;
}
}
Теперь пришло время для запуска, развертывания, тестирования и устранения неполадок, если это необходимо. Для разработки я использую CentOS 6, которая по умолчанию включает Python 2.7. Помните, что мы должны взять Python 3.5 или более поздней версии.
Примечание
Листинги 6.9–6.12 находятся в исходниках книги в Workspace\Ch06\battleship\index.html, как и ресурсы, необходимые для развертывания игры в облаке.
В главе 3 объясняется, как установить и запустить Python 3.6 и Python 2.7 по отдельности. В данном конкретном случае сценарий-обертка используется для активации Python 3 на сервере CGI перед вызовом квантовой программы:
#!/bin/sh
# Домашняя папка
root=/home/centos
program=qbattleship.py
# Активация python 3
source $root/qiskit/qiskit/bin/activate
# Выполнение квантовой программы на python
python $program
В предыдущем сценарии просто активируется Python 3 и вызывается реальная квантовая программа qbattleship.py. Это необходимо, иначе веб-сервер будет использовать версию Python по умолчанию (2.7) и программа завершится с ошибкой, поскольку для QISKit требуется Python 3.5 или более поздней версии. Помните, что среда Python 3 была создана в домашней папке пользователя следующим образом:
$ mkdir –p $HOME/qiskit
$ cd $HOME/qiskit
$ python3.6 -m venv qiskit
Активация виртуальной среды:
$ source qiskit/bin/activate
Теперь наконец перейдем к развертыванию и тестированию. Надеюсь, что устранять ошибки не потребуется.
В этом разделе мы развернем игру на HTTPD-сервере Apache и посмотрим на нее в действии. Полный исходный код игры, включая все файлы поддержки, стили, изображения, оболочку CGI и квантовую программу, можно найти в исходниках книги, в разделе Workspace\Ch06\battleship. Расположение папок показано на рис. 6.6.
Примечание
В этом разделе предполагается, что вы уже установили HTTPD-сервер Apache в своей системе и служба по умолчанию настроена и работает правильно.
Если это не так, исправить ситуацию можно, обратившись к учебным пособиям. Например, для CentOS 7 мне нравится www.liquidweb.com/kb/how-to-install-apache-on-centos-7/.
Рис. 6.6. Расположение папок для облачной Quantum Battleship
1. Создайте папку с названием public_html в своей домашней папке пользователя:
$ mkdir $HOME/public_html
2. Создайте папку cgi-bin в public_html для хранения CGI-сценариев Python:
$ mkdir $HOME/public_html/cgi-bin
3. Выполните конфигурацию сервера HTTPD, чтобы разрешить доступ как из public_html, так и из public_html/cgi-bin (листинг 6.15). Обратите внимание, что для cgi-bin требуется специальное разрешение для выполнения CGI-сценариев.
4. Если хотите использовать исходный код книги, скопируйте все файлы из Workspace\Ch06\battleship to public_html/battleship.
5. Убедитесь, что права доступа к файлам установлены корректно для папки public_html и всех вложенных папок и файлов. Это очень важно: если разрешения неправильные, браузер выдаст ответ «500 — Internal Server Error». Когда я выполнял тестирование на своем персональном компьютере с CentOS 6, основным источником головной боли было $chmod-R755public_html.
Листинг 6.15. Конфигурация, разрешающая HTTP-запросы из папки public_html (CentOS 6/Apache HTTPD 2.2)
<IfModule mod_userdir.c>
# UserDir disabled
#
# Чтобы разрешить запросам к /~user/ обращаться к папке public_html,
# удалите строку "UserDir disabled" выше и взамен раскомментируйте
# следующую строку:
#
UserDir public_html
</IfModule>
<Directory /home/*/public_html>
AllowOverride FileInfo AuthConfig Limit
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+ExecCGI
AddHandler cgi-script .cgi
<Limit GET POST OPTIONS>
Order allow,deny
Allow from all
</Limit>
</Directory>
<Directory "/home/*/public_html/cgi-bin">
AllowOverride None
Options ExecCGI
SetHandler cgi-script
</Directory>
Совет
Чтобы разрешить запросы из public_html (см. листинг 6.15), необходимо, чтобы в httpd.conf был включен модуль userdir для Apache (раскомментируйте LoadModule userdir_module modules / mod_userdir.so). Этот модуль может быть заблокирован по умолчанию.
Скопируйте сценарий из листинга 6.15 в системную папку /etc/httpd/conf.d. Она содержит файлы конфигурации, автоматически загружаемые HTTPD-сервером Apache при запуске. Теперь запустите сервер HTTPD в CentOS (обратите внимание: предполагается, что в вашей системе уже установлен HTTPD-сервер Apache):
$ sudo service httpd start (CentOS 6)
$ sudo systemctl start httpd (CentOS 7)
Наконец, в качестве финального аккорда запустите браузер и перейдите по URL-адресу http://localhost/~centos/battleship/ (при условии, что имя пользователя — centos). Надеюсь, проблем не будет и вы сможете начать играть в Quantum Battleship в облаке. Но если что-то пойдет не так, изучите приведенный далее список проблем, с которыми я столкнулся при настройке.
Большинство проблем, с которыми я столкнулся, были связаны с правами доступа к файлам из-за моей застарелой привычки использовать старый добрый HTTPD-сервер Apache.
• Особенность HTTPD-сервера Apache заключается в следующем: чтобы разрешить запросы из домашней папки пользователя (см. листинг 6.15), необходимо, чтобы модуль userdir был включен в конфигурации демона httpd.conf. В зависимости от вашей ОС этот модуль может быть заблокирован по умолчанию. Пользователи HTTPD 2.4 должны учесть: листинг 6.15 предназначен для Apache v2.2, а v2.4 может потребоваться другой синтаксис.
• HTTP-статус 500 — внутренняя ошибка сервера в браузере: убедитесь, что права доступа к файлам для public_html и всех вложенных файлов и папок установлены на 755. Вы можете диагностировать это, посмотрев файлы журналов HTTPD, расположенные по адресам:
/var/log/httpd/error_log
/var/log/httpd/suexec.log
Например, вот фрагмент из suexec.log, в котором говорится, что мои разрешения некорректны:
$ tail -f /var/log/httpd/suexec.log
[2018-04-02 17:03:45]: cannot get docroot information (/home/centos)
[2018-04-02 17:10:13]: uid: (500/centos) gid: (500/centos) cmd: first.cgi
[2018-04-02 17:10:13]: directory is writable by others: (/home/centos/ public_html)
Примечание
suEXEC — это функция веб-сервера Apache. Он позволяет пользователям запускать приложения CGI и SSI от имени другого пользователя. В CentOS suEXEC записывает журналы в /var/log/httpd/suexec.log.
• Проблемное место SELinux — модуль безопасности ядра Linux, который обеспечивает механизм поддержки политик безопасности контроля доступа. В CentOS данная функция включена по умолчанию. Ее можно отключить временно в командной строке с помощью команды:
$ sudo setenforce 0
или постоянно, отредактировав файл /etc/sysconfig/selinux и установив для ключа SELINUX значение disabled:
$ sudo vi /etc/sysconfig/selinux
SELINUX=disabled
SELINUXTYPE=targeted
Обратите внимание, что SELinux может стать причиной проблем при вызове сценариев CGI или когда квантовая программа пытается выполнить код удаленно на моделирующем устройстве IBM либо на реальном устройстве.
• Ошибки Python: если в сценарии Python возникает какая-либо ошибка, обработчик исключений CGI перехватывает ее и выдает красивую HTML-страницу в текущем рабочем каталоге (cgi-bin). На рис. 6.7 показан вывод ошибки превышения лимита времени при выполнении на реальном квантовом устройстве ibmqx2.
Рис. 6.7. Вывод ошибок в Python, созданный пакетом cgi
• Проблемы конфигурации API: если вы работаете на реальном квантовом устройстве, убедитесь, что конфигурация в Qconfig.py корректна (включая токен API для вашей учетной записи Q Experience), как показано в следующем фрагменте:
APItoken = 'YOUR-API-TOKEN'
config = {
'url': 'https://quantumexperience.ng.bluemix.net/api',
}
Обратите внимание, что Qconfig.py должен находиться в том же месте, что и квантовая программа qbattleship.py, то есть в папке cgi-bin. Тем не менее в игру можно внести дополнительные улучшения. Обсудим их в следующем разделе.
В Cloud Battleship, рассмотренной в предыдущем разделе, можно внести некоторые улучшения — наверняка вы сами заметите это, поиграв какое-то время. Вот список моих идей.
• Интерфейс пользователя отображает игровые поля с кораблями и бомбами для обоих игроков. В настоящей игре «Морской бой» каждый игрок должен открыть собственное окно браузера, установить свои корабли и начать бомбардировку противника.
• Игровое состояние: корабль, позиции бомб и квантовое устройство хранятся на стороне клиента из-за того, что HTTP — это протокол без сохранения состояния. То есть приходит запрос, запускается программа на Python, и ответ отправляется обратно. После этого вся память стирается. Настоящая игра должна использовать серверный лобби-клиент игрока для размещения игрового состояния (например, с применением сервера приложения) и координации взаимодействия между окнами браузера.
Максимально улучшенная версия игры Cloud Quantum Battleship должна использовать два окна браузера для двух игроков с игровыми полями для размещения кораблей и бомб в каждом. Кроме того, HTTPD-сервер Apache нужно заменить сервером приложения, таким как Apache Tomcat, который способен хранить состояние игры. Схема показана на рис. 6.8.
• Элементарный лобби-клиент игры можно реализовать в виде веб-приложения Tomcat для хранения кораблей, позиций бомб и квантовых устройств.
• Веб-приложение может использовать средства времени выполнения операционной системы хоста, в данном случае API времени выполнения Java, чтобы вызвать квантовый сценарий Python, вернуть результаты нанесенного ущерба и отправить их каждому игроку.
• Чтобы избежать раздражающего постоянного обновления страниц, каждый браузер может подключаться через WebSocket к серверу приложений. Это позволит поддерживать постоянное соединение, а клиенты смогут быстро обмениваться текстовыми сообщениями JSON.
Рис. 6.8. Улучшенная версия Cloud Quantum Battleship
Для того чтобы соединение происходило через WebSocket вместо AJAX, веб-страницу интерфейса пользователя следует немного изменить, как показано в листинге 6.16.
Совет
Проект Eclipse для этого раздела находится в исходниках книги в Workspace\Ch06_BattleShip. Учитывая, что некоторые детали этого веб-приложения сложно реализуются, рекомендую открыть рабочее пространство в своей IDE и дальше читать, параллельно работая с ним. Надеюсь, что вы хорошо умеете писать приложения с помощью Eclipse/Tomcat.
Листинг 6.16. Код клиента WebSocket на JavaScript в WebContent/js/websocket.js
// Конечная точка сервера WS (file: websocket.js)
var END_POINT = "ws://localhost:8080/BattleShip/WSBattleship";
// Случайный ID, используемый для отслеживания клиента
var CLIENT_ID = Math.floor(Math.random() * 10000);
function WS_connect(host) {
LOGD("WS Connect " + host);
if ('WebSocket' in window) {
this.socket = new WebSocket(host);
} else if ('MozWebSocket' in window) {
this.socket = new MozWebSocket(host);
} else {
LOGE('Error: WebSocket is not supported by this browser.');
return;
}
this.socket.onopen = function() {
LOGD('WS Opened ' + host);
};
this.socket.onclose = function() {
LOGD('WS Closed ' + host);
};
this.socket.onmessage = function(message) {
// { status: 200 , message :'…'}
LOGD('OnMessage: ' + message.data);
var json = JSON.parse(message.data);
if ( json.status >= 300 && json.status < 400) {
// Предупреждение
notify(json.message, 'warning');
}
if ( json.status >= 400 ) {
// Ошибка
notify(json.message, 'danger');
return;
}
handleResponse (json);
};
}
function WS_initialize () {
var clientId = CLIENT_ID;
var host = END_POINT;
this.url = host + '?clientId=' + clientId;
WS_connect(this.url);
};
function WS_send (text) {
this.socket.send(text);
};
На стороне клиента:
• все основные браузеры реализуют стандарт WebSocket, который используется для поддержки долговременного соединения с сервером. Для этого в строке 2 создана конечная точка ws://localhost:8080/BattleShip/WSBattleship. Обратите внимание, что этот параметр можно отправить в список конечных точек WebSocket как обычный URL. Таким образом, конечным URL для WS будет ws://localhost:8080/BattleShip/WSBattleship?clientId=RANDOM-ID, где для отслеживания каждого из игроков применяется случайный ID;
• WebSocket использует в JavaScript системный обратный вызов для получения следующих событий:
• socket.onopen — вызывается при открытии сокета, в строке 23 показан обратный вызов для обработки этого события;
• socket.onclose — вызывается при разрыве соединения, например, когда браузер был закрыт или обновился либо упал сервер;
• socket.onmessage — это самая важная функция обратного вызова, которая выполняется при получении сообщений в формате JSON, отправленных Python, как это было с AJAX в предыдущей версии.
При загрузке страницы в браузере игрока клиент устанавливает соединение при помощи обратного вызова DOM window.onload:
function win_onload () {
WS_initialize ();
}
window.onload = win_onload;
На серверной стороне нам нужен сервер приложений с поддержкой WebSocket. К счастью, Tomcat в полной мере реализует стандарт WebSocket во всех операционных системах. В листинге 6.17 показана базовая реализация обработчика событий на стороне сервера WebSocket на Java.
Листинг 6.17. Структура обработчика событий на стороне сервера WebSocket (WSConnector.java)
@ServerEndpoint(value = "/WSBattleship")
public class WSConnector {
// Установка соединений
private static final List<WSConnectionDescriptor> connections =
new CopyOnWriteArrayList<WSConnectionDescriptor>();
// Данные из игры Player-ID => {name: 'Player-1', ships: "0,0,0:,
// bombs: "0,0,0,0,0 }
private static final Map<String, JSONObject> data =
new HashMap<String, JSONObject>();
/** Id клиента для данного WS */
String clientId;
private String getSessionParameter (Session session, String key) {
if ( ! session.getRequestParameterMap().containsKey(key)) {
return null;
}
return session.getRequestParameterMap().get(key).get(0);
}
@OnOpen public void open(Session session) {
clientId = getSessionParameter(session, "clientId");
// Проверка id клиента на уникальность
WSConnectionDescriptor conn = findConnection(clientId);
if ( conn != null) {
unicast(conn.session,
WSMessages.createStatusMessage(400
, "Rejected duplicate session.").toString());
}
else {
connections.add(new WSConnectionDescriptor(clientId, session));
}
dumpConnections("ONOPEN " + clientId );
}
@OnClose
public void end() {
}
@OnMessage
public void incoming(String message) {
WSConnectionDescriptor d = findConnection(clientId);
try {
JSONObject root = new JSONObject(message);
String name = root.getString("name");
String action = root.optString("action");
// перезапустить игру?
if ( action.equalsIgnoreCase("reset")) {
multicat(WSMessages.createStatusMessage(300
, "Game reset by " + name).toString());
data.clear();
return;
}
// Валидация правил игры…
// Выполнение скрипта Python
linuxExecPython(args);
} catch (Exception e) {
LOGE("OnMessage", e);
}
}
@OnError
public void onError(Throwable t) throws Throwable {
LOGE("WSError: " + t.toString());
}
}
В Java обработчики событий на стороне сервера WebSocket реализованы в соответствии со стандартом для аннотаций J2EE, что позволяет всем разработчикам повторно использовать код.
• В строке 1 листинга 6.17 в классе Java WSConnector определяется аннотация @ServerEndpoint(value="/WSBattleship"). И это все, что нам нужно для создания обработчика событий на стороне сервера. Значение WSBattleship соответствует названию обработчика событий, таким образом, полное название конечной точки соединения с сервером будет ws://host:POT/Battleship/WSvattleship?QUERY-STRING.
• Обратные вызовы для событий открытия, закрытия и выдачи сообщения объявляются с помощью аннотаций @OnOpen, @OnClose и @OnMessage соответственно.
Обратите внимание на то, что название метода несущественно, значимыми являются следующие параметры:
• OnOpen — получает объект Session, который содержит информацию о соединении;
• OnClose — без параметров, выполняется при завершении соединения с браузером;
• OnMessage — самый важный вызов в этом наборе. Он выполняется, когда клиент отправляет текстовое сообщение с данными в качестве параметра.
• Имейте в виду, что для каждого клиентского соединения будет создано по одному экземпляру класса WSConnector. В строке 5 для отслеживания всех клиентских соединений используется потокобезопасный статический список List<WSConnectionDescriptor>. В строке 5 объявляется статическая хеш-таблица для игровых данных, где ключом является ID игрока, а значением — объект JSON, отправленный браузером, например [Player-1=>{name:'Player-1’,ships:"0,0,0:,bombs:"0,0,0,0,0",device:"local_qasm_simulator"}].
• Когда происходит обратный вызов при получении сообщения (строки 201–253 в файле WSConnector.java), текстовое сообщение преобразуется в формат JSON, данные сохраняются в памяти, применяются правила игры и, если все корректно, вызывается скрипт Python с позициями кораблей и бомб. В конце полученные результаты отправляются каждому клиенту для обновления.
Для отправки сообщения на сторону клиента можно использовать объект Session session.getBasicRemote().sendText("SomeText").
Чтобы отправить групповое сообщение всем клиентам, можно воспользоваться списком сообщений:
static void multicast ( String message ) {
for ( WSConnectionDescriptor conn : connections) {
conn.session.getBasicRemote().sendText(message)
}
}
Несмотря на то что язык Java спроектирован так, чтобы быть независимым от ОС, системные команды можно запустить с помощью системного вызова Runtime.getRuntime().exec("command").
В листинге 6.18 показан очень простой класс для выполнения команды и считывания ее стандартного вывода в строковый буфер.
Листинг 6.18. Выполнение системных команд и извлечение результатов (SysRunner.java)
public class SysRunner {
final String command;
final StringBuffer stdout = new StringBuffer();
final StringBuffer stderr = new StringBuffer();
public SysRunner(String command) {
this.command = command;
}
public void run () throws IOException, InterruptedException {
final Process process = Runtime.getRuntime().exec(command);
pipeStream(process.getInputStream(), stdout);
pipeStream(process.getErrorStream(), stderr);
process.waitFor();
}
private void pipeStream (InputStream is, StringBuffer buf) throws
IOException {
BufferedReader br = new BufferedReader(new
InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
buf.append(line);
}
}
public StringBuffer getStdOut () {
return stdout;
}
public StringBuffer getStdErr () {
return stderr;
}
}
Чтобы получить выходные данные команды, используйте входной поток процесса, прочитайте из него данные и сохраните в строковом буфере (строки 17–24 в листинге 6.18): pipeStream(process.GetInputStream(),stdout). Теперь у нас есть инструмент для запуска программы на Python, но нам все еще нужно иметь дело с правами доступа к файлам в Linux. Помните, что скрипт Python необходимо включить в само веб-приложение (рис. 6.9). Поэтому, когда сервер приложений извлечет в файловую систему веб-приложение Battleship вместе с кодом Python, сценарий получит установленное по умолчанию право доступа к файлу 644 (не исполняемому в среде). Это приведет к сбою сценария при запуске.
Рис. 6.9. Схема проекта J2EE для Cloud Battleship
Чтобы установить корректные права доступа к файлам для кода Python внутри веб-приложения, выполните системную команду chmod с названиями файлов, как показано в следующем отрывке кода:
// Получить путь к основному каталогу для кода Python
// …webapps/BattleShip/python/
String root = IOTools.getResourceAbsolutePath("/") + "../../";
// Нельзя использовать специальные символы *&$#
String cmd = "/bin/chmod 755 " + base + "python" + File.separator;
String[] names = { "Qconfig.py", "qiskit-basic-test.py"
, "qiskit-driver.sh", "qbattleship-sim.py", "qbattleship.py"};
for (int i = 0; i < names.length; i++) {
SysRunner r = new SysRunner(cmd + names[i]);
r.run();
}
Путь к основному каталогу установки приложения в Java можно получить с помощью отражения (reflection), как показано далее:
public static String getResourceAbsolutePath(String resourceName) throws
UnsupportedEncodingException {
URL url = IOTools.class.getResource(resourceName);
String path = URLDecoder.decode(url.getFile(), DEFAULT_ENCODING);
// path -> Windows: /C:/…/Workspaces/…/
// path-> Linux: /home/users/foo…
if ( path.startsWith("/") && OS_IS_WINDOWS) {
// Должен быть удален первый символ / (только в Windows!)
path = path.replaceFirst("/", "");
}
return path;
}
Наконец, квантовую программу на Python можно выполнить из обратного вызова при получении сообщения WebSocket, как показано в листинге 6.19.
Листинг 6.19. Выполнение квантовой программы и отправление результатов на сторону клиента
// Аргументы: ships1=0,0,0 ships2=0,0,0 bombs1=0,0,0,0,0 bombs2=0,0,0,0,0
// device=local_qasm_simulator
private void linuxExecPython (String args) throws Exception {
// STDOUT {status: 200, message: 'Some text', damage:
// [[0,0,0,0,0],[0,0,0,0,0]]}
StringBuffer stdout = IOTools.executePython(SCRIPT_ROOT, args);
JSONObject resp = new JSONObject(stdout.toString());
// Отправить назад клиентам в обратном порядке
JSONArray damage = resp.getJSONArray("damage");
resp.remove("damage");
final int size = damage.length() – 1;
for (int i = 0; i < connections.size(); i++) {
resp.put("damage", damage.get( size — i));
unicast(connections.get(i).session, resp.toString());
resp.remove("damage");
}
}
// Основной каталог: WEPAPP_PATH/python/qiskit-driver.sh
// Аргументы: WEPAPP_PATH/python/qbattleship.py
// 0,0,0 0,0,0 0,0,0,0,0 0,0,0,0,0 device
public static StringBuffer executePython (String base, String args)
throws IOException, InterruptedException {
String driver = base + File.separator + "python" + File.separator +
"qiskit-driver.sh";
String program = base + File.separator + "python" + File.separator +
"qbattleship.py";
String cmd = driver + " " + program + ( args != null ? " " + args : "");
SysRunner r = new SysRunner(cmd);
r.run();
return r.getStdOut();
}
Чтобы выполнить квантовую программу на Python, код в листинге 6.19:
• получает расположение (LOCATION) каталога с кодом на Python внутри веб-приложения. Это TOMCAT-ROOT/webapps/Battleship/python;
• выполняет скрипт драйвера LOCATION/qiskit-driver.sh LOCATION/qbattleship.py с аргументами:
• ships1 — расположение кораблей первого игрока;
• ships2 — расположение кораблей второго игрока;
• bombs1 — счетчики бомб первого игрока;
• bombs2 — счетчики бомб второго игрока;
• device — квантовое устройство;
• отправляет результаты клиентам.
Наконец, из своей IDE экспортируйте веб-архив Cloud Quantum Battleship (WAR), разверните его в контейнере Tomcat и играйте из двух веб-браузеров, перейдя по адресу http://localhost:8080/BattleShip/ (рис. 6.10). И хотя я не сомневаюсь в том, что у вас достаточно опыта, все же на всякий случай приведу порядок действий.
1. Экспортируйте веб-приложение как веб-архив WAR и щелкните правой кнопкой мыши на проекте Ch06_Battleship в своей IDE (см. рис. 6.9). Нажмите ExportWeb Archive (ЭкспортироватьВеб-архив) и выберите файл (то есть Ch06_Battleship.war).
2. Убедитесь в том, что Tomcat запущен и выполняется. Если он не установлен в вашей ОС по умолчанию, то вам помогут следующие команды:
yum -y install java (CentOS 6,7)
yum -y install tomcat7 tomcat7-webapps tomcat7-admin-webapps
(CentOS 6,7)
service tomcat7 start (CentOS 6)
systemctl start tomcat7 (CentOS 7)
3. Используйте интерфейс пользователя менеджера Tomcat, находящийся по адресу http://yourhost:8080/manager/, чтобы загрузить и развернуть архив в своем контейнере Tomcat в Linux. (Совет: менеджер запросит имя пользователя и пароль, если у вас их нет, отредактируйте файл /etc/tomcat7/tomcat-users.xml.)
4. Теперь у вас должна быть возможность указать два браузера на http://localhost:8080/BattleShip/ (совет: веб-приложения Tomcat развернуты в папке /var/lib/tomcat7/webapps). Есть проблемы? Проверьте журналы контейнера в /var/log/tomcat7/catalina.out.
Рис. 6.10. Улучшенная версия Cloud Battleship с двумя браузерами
В этой главе было показано, как популярную игру Battleship можно запустить на квантовом компьютере с применением однокубитных частичных вентилей НЕ для вычисления степени повреждения корабля. С этой целью был использован пример Quantum Battleship из учебника по QISKit.
Кроме того, мы вывели игру на новый уровень, значительно улучшив пользовательский интерфейс. Вы узнали, как квантовый код можно вызвать в облаке с помощью сценариев CGI через сервер Apache HTTPD. Дальнейшие улучшения были сделаны, чтобы можно было играть сразу в двух браузерах через контейнер Tomcat J2EE. Код Eclipse для обоих проектов доступен в исходниках книги в папках Workspace\Ch06 и Workspace\Ch06_BattleShip соответственно.
В следующей главе рассматриваются две игры-головоломки, которые демонстрируют удивительную мощь квантовых алгоритмов по сравнению с их классическими аналогами: загадка про фальшивую монету и магический квадрат Мермина — Переса. Это примеры квантовой псевдотелепатии, или способности игроков достигать результатов, которые возможны, только если они во время игры читали мысли друг друга.