在量子戰艦遊戲中,前端使用 JavaScript 實作了玩家回合的檢查、炸彈計數的更新、資料的提交以及後端回應的處理。遊戲的核心邏輯圍繞著炸彈計數的差異判斷玩家回合,並透過非同步請求與後端互動。後端使用 Python 和 QISKit 處理量子計算部分,並將結果傳回前端更新遊戲介面。佈署方面,本文詳細介紹了在 Apache HTTPD 上佈署遊戲的流程,包括設定虛擬環境、組態伺服器以及檔案許可權設定等關鍵步驟,同時提供了常見錯誤的解決方案。為了進一步提升遊戲體驗,文章還探討了使用 WebSocket 技術和應用程式伺服器(如 Apache Tomcat)實作即時通訊和遊戲狀態管理的可能性,為遊戲提供了參考。

量子戰艦遊戲前端實作解析

玩家回合檢查與炸彈計數邏輯

在量子戰艦遊戲中,玩家回合的檢查是透過比較兩個玩家的炸彈計數來實作的。程式碼中定義了一個變數 dif,用於計算當前玩家與另一位玩家的炸彈計數差異。

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));
}

內容解密:

  1. dif 的計算:比較當前玩家與另一位玩家的炸彈計數,判斷是否輪到當前玩家操作。
  2. dif >= 2 時,表示不是當前玩家的回合,進行錯誤處理。
  3. 重置當前玩家的炸彈選擇狀態,並顯示錯誤訊息。

炸彈計數與前端更新

當玩家進行有效的操作時,程式會更新炸彈計數並反映在前端介面上。

BOMBS[player - 1][qubit]++;
$('#b' + player + qubit).html("(" + BOMBS[player - 1][qubit] + ")");
notify('Bomb ready. Click Submit', 'info');
mustSubmit = true;

內容解密:

  1. 更新指定玩家的炸彈計數陣列 BOMBS
  2. 將更新後的炸彈計數顯示在前端對應的元素中。
  3. 顯示通知訊息,提示玩家點選「Submit」按鈕。
  4. 設定 mustSubmittrue,表示資料已準備好提交。

資料提交與後端互動

當玩家點選「Submit」按鈕時,前端會將相關資料非同步傳送至後端。

function submit() {
    var url = "cgi-bin/qiskit-driver.sh";
    var data = 'ships1=' + s1 + '&ships2=' + s2 + '&bombs1=' + BOMBS[0].join(',') + '&bombs2=' + BOMBS[1].join(',') + '&device=' + $('#device').val();
    $.get(url, data)
        .done(function(json) {
            handleResponse(json);
        })
        .fail(function() {
            notify('Internal Server Error. Check logs.', 'danger');
        });
}

內容解密:

  1. 組裝請求資料,包括兩名玩家的艦船位置、炸彈計數及所選用的量子裝置。
  2. 使用 jQuery 的 $.get 方法將資料非同步傳送至指定的後端 URL。
  3. 成功時呼叫 handleResponse 處理後端回應,失敗時顯示錯誤通知。

後端回應處理

後端回應的處理邏輯包含解析回應資料、更新介面顯示及判斷遊戲勝負。

function handleResponse(json) {
    var damage = json.damage;
    var d1 = damage[0];
    var d2 = damage[1];
    for (var i = 0; i < 5; i++) {
        var pct1 = (d1[i] * 100).toFixed(1);
        var pct2 = (d2[i] * 100).toFixed(1);
        // 更新介面顯示損傷百分比
    }
    // 判斷遊戲勝負
    var s1 = d1.reduce(function(total, currentValue) { return total + currentValue; }, 0);
    var s2 = d2.reduce(function(total, currentValue) { return total + currentValue; }, 0);
    if (s1 > 2.85) winner = 2;
    if (s2 > 2.85) winner = 1;
    if (winner != 0) {
        notify('** G.A.M.E O.V.E.R Player ' + winner + ' wins **', 'success');
        gameover = true;
    }
}

內容解密:

  1. 從後端回應中提取損傷資料,並計算每個艦船的損傷百分比。
  2. 根據損傷百分比更新介面顯示,並對沉沒的艦船發出通知。
  3. 累加所有艦船的損傷值,判斷是否達到勝利條件(總損傷值 > 2.85)。
  4. 若有玩家獲勝,顯示遊戲結束訊息並設定 gameovertrue

重置遊戲狀態

當玩家點選「Reset」按鈕時,遊戲狀態會被重置。

function reset_click() {
    gameover = false;
    for (var i = 0; i < 5; i++) {
        $('#p1s' + i).attr('checked', false);
        $('#p2s' + i).attr('checked', false);
        // 重置其他相關介面元素
    }
}

內容解密:

  1. gameover 狀態設為 false,允許遊戲重新開始。
  2. 重置所有與遊戲狀態相關的前端元素,包括核取方塊、顯示資訊等。

在雲端佈署量子戰艦遊戲

在前面的章節中,我們已經完成了量子戰艦遊戲的開發。現在,是時候來執行、佈署、測試,並在必要時進行故障排除。本章節將介紹如何在Apache HTTPD伺服器上佈署遊戲,並探討可能出現的問題及其解決方案。

執行多版本Python

第三章已經介紹瞭如何安裝和執行Python 3.6和Python 2.7。在這個特定的案例中,我們使用了一個包裝指令碼來在CGI後端啟用Python 3,然後呼叫量子程式。

#!/bin/sh
# home dir
root=/home/centos
program=qbattleship.py
# 啟用Python 3
source $root/qiskit/qiskit/bin/activate
# 執行Python量子程式
python $program

這個指令碼的作用是啟用Python 3環境,並執行qbattleship.py程式。如果不這樣做,Web伺服器將預設使用Python 2.7,而QISKit需要Python 3.5或更高版本。

內容解密:

  1. source $root/qiskit/qiskit/bin/activate:這行命令用於啟用Python 3虛擬環境。source命令用於執行指設定檔案中的命令,這裡是啟用虛擬環境的指令碼。
  2. python $program:這行命令執行qbattleship.py程式,使用當前啟用的Python環境(即Python 3)。

佈署和測試

在本文中,我們將遊戲佈署在Apache HTTPD伺服器上,並觀察遊戲的執行情況。遊戲的完整原始碼,包括所有支援檔案、樣式、影像、CGI包裝器和量子程式,都可以在書籍原始碼的Workspace\Ch06\battleship目錄下找到。檔案結構如圖6-6所示。

步驟:

  1. 在使用者的主目錄下建立一個名為public_html的資料夾。

    $ mkdir $HOME/public_html
    
  2. public_html下建立cgi-bin資料夾,用於存放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複製到public_html/battleship

  5. 確保public_html資料夾及其所有子資料夾和檔案的檔案許可權正確。這非常重要;如果許可權不正確,瀏覽器將傳回“500 - 內部伺服器錯誤”的回應。

    $ chmod -R 755 public_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>

疑難排解

在設定過程中,大部分問題都與檔案許可權有關,這是由於對Apache HTTPD的不熟悉所致:

  • Apache HTTPD的特殊性:要啟用來自使用者主目錄的請求(清單6-15),需要在httpd.conf中啟用userdir模組。根據您的作業系統,此模組可能預設未啟用。此外,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
  • SELinux問題:這是一個Linux核心安全模組,提供了一種支援存取控制安全策略的機制。在CentOS中,此功能預設啟用。可以透過命令列暫時停用它:
    $ sudo setenforce 0
    

最終,開啟瀏覽器並存取URL http://localhost/~centos/battleship/(假設使用者名稱是centos),即可開始玩雲端的量子戰艦遊戲。希望您能夠順利執行,如果遇到問題,可以參考上述的疑難排解。

雲端量子戰艦遊戲的進階開發與WebSocket技術應用

在前一章的雲端戰艦遊戲開發基礎上,本章將進一步探討如何透過引入WebSocket技術和使用應用程式伺服器(如Apache Tomcat)來提升遊戲的互動性和穩定性。透過這些技術的應用,我們可以實作更真實的遊戲體驗,例如每個玩家使用獨立的瀏覽器視窗進行遊戲,並在伺服器端儲存遊戲狀態。

進一步的改進

雲端量子戰艦遊戲仍有許多可以改進的地方,主要包括以下幾個方面:

  1. 使用者介面最佳化:目前的遊戲版本在同一個瀏覽器視窗中顯示雙方玩家的艦船和炸彈佈局。在真實的遊戲場景中,每個玩家應該在自己的瀏覽器視窗中設定艦船並對對手進行攻擊。

  2. 遊戲狀態管理:由於HTTP協定是無狀態的,遊戲狀態(如艦船位置、炸彈位置和量子裝置狀態)目前儲存在客戶端。為了實作更穩定的遊戲體驗,應該使用根據伺服器的玩家大廳來儲存遊戲狀態,並協調不同瀏覽器視窗之間的通訊。

更好的雲端量子戰艦遊戲

為了實作更好的雲端量子戰艦遊戲,需要對現有的架構進行升級,主要改進包括:

  • 使用兩個獨立的瀏覽器視窗分別代表兩個玩家,每個視窗包含艦船和炸彈佈局。
  • 將Apache HTTPD替換為能夠儲存遊戲狀態的應用程式伺服器,如Apache Tomcat。
  • 實作一個基本的遊戲大廳作為Tomcat網路應用,以儲存艦船位置、炸彈位置和量子裝置狀態。
  • 使用Java的Runtime API呼叫量子Python指令碼,取得損害結果,並將其分派給各個玩家。
  • 為了避免頻繁的瀏覽器頁面重新整理,使用WebSocket技術保持與應用程式伺服器的持久連線,以便快速傳送JSON文字訊息。

WebSocket技術的應用

為了實作實時通訊,客戶端的網頁需要透過WebSocket連線到應用程式伺服器,而不是使用AJAX。以下是客戶端WebSocket JavaScript程式碼的範例(來自websocket.js):

// 伺服器WebSocket端點
var END_POINT = "ws://localhost:8080/BattleShip/WSBattleship";
// 用於追蹤客戶端的隨機ID
var CLIENT_ID = Math.floor(Math.random() * 10000);

function WS_connect(host) {
    // ...
}

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標準,能夠與支援WebSocket的伺服器保持持久連線。
  • WebSocket端點URL格式為ws://localhost:8080/BattleShip/WSBattleship,並可附加引數,如客戶端ID。

WebSocket JavaScript事件處理

WebSocket使用回呼函式處理事件,如socket.onopensocket.onclosesocket.onmessage

this.socket.onmessage = function(message) {
    var json = JSON.parse(message.data);
    // 處理接收到的JSON訊息
    handleResponse(json);
}

伺服器端的WebSocket處理

在伺服器端,需要使用支援WebSocket的應用程式伺服器,如Apache Tomcat。以下是Java實作的WebSocket處理程式骨架(來自WSConnector.java):

@ServerEndpoint(value = "/WSBattleship")
public class WSConnector {
    // 連線列表
    private static final List<WSConnectionDescriptor> connections = new CopyOnWriteArrayList<>();
    // 遊戲資料
    private static final Map<String, JSONObject> data = new HashMap<>();

    /** 客戶端ID */
    String clientId;

    // ...
}

內容解密:

  1. @ServerEndpoint(value = "/WSBattleship"):此註解標記了WebSocket的端點URL,客戶端將透過此URL連線到伺服器。
  2. private static final List<WSConnectionDescriptor> connections:用於儲存所有活躍的WebSocket連線,確保能夠向所有連線的客戶端廣播訊息。
  3. private static final Map<String, JSONObject> data:儲存遊戲的相關資料,如玩家ID對應的遊戲狀態,以便管理和更新遊戲資料。

Java WebSocket 伺服器端實作與量子戰艦遊戲整合

在 Java 中,WebSocket 伺服器端處理器是透過 J2EE 註解標準來實作的,這使得程式碼能夠在所有供應商之間重複使用。

WebSocket 伺服器端處理器實作

@ServerEndpoint(value = "/WSBattleship")
public class WSConnector {
    private static List<WSConnectionDescriptor> connections = new ArrayList<>();
    private 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");
        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")) {
                multicast(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());
    }
}

內容解密:

  1. @ServerEndpoint(value = "/WSBattleship"):定義 WebSocket 伺服器端點。
  2. @OnOpen@OnClose@OnMessage@OnError:分別處理連線開啟、關閉、接收訊息和錯誤事件。
  3. getSessionParameter:從 Session 中取得指定引數的值。
  4. open 方法中,檢查客戶端 ID 是否重複,若重複則拒絕連線。
  5. incoming 方法中,處理接收到的訊息,包括解析 JSON、驗證遊戲規則和執行 Python 指令碼。

執行作業系統命令與 Python 指令碼

為了執行 Python 指令碼,需要使用 Runtime.getRuntime().exec("command") 系統呼叫。

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;
    }
}

內容解密:

  1. SysRunner 類別:用於執行作業系統命令並擷取輸出結果。
  2. run 方法:執行命令並將輸出結果導向 stdoutstderr
  3. pipeStream 方法:將輸入串流的內容附加到指定的 StringBuffer

設定檔案許可權

由於 Python 指令碼需要被執行,因此需要設定正確的檔案許可權。

// 取得 Python 程式碼的基本路徑
String root = IOTools.getResourceAbsolutePath("/") + "../../";
// 設定 chmod 命令
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();
}

內容解密:

  1. 取得基本路徑:使用 IOTools.getResourceAbsolutePath 方法取得 Python 程式碼的路徑。
  2. 設定 chmod 命令:使用 chmod 命令設定 Python 指令碼的檔案許可權為 755。

結合 WebSocket 與量子戰艦遊戲

透過 WebSocket 伺服器端處理器,可以實作與客戶端的即時通訊,並結合量子戰艦遊戲的邏輯。

// 在 @OnMessage 方法中執行 Python 指令碼
linuxExecPython(args);

內容解密:

  1. 執行 Python 指令碼:在接收到客戶端的訊息後,執行對應的 Python 指令碼。
  2. 結合遊戲邏輯:根據客戶端的輸入,執行相應的遊戲邏輯,並將結果回傳給客戶端。