iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
JavaScript

重新拿回思考力!一起來用 JavaScript 打造 CLI 好玩遊戲區!系列 第 21 篇

Day21 共用入口2:觀察 bug 是怎麼發生的

  • 分享至 

  • xImage
  •  

今天延續昨天的問題,我們看到了畫面是長這樣的:

1

所以今天的主題是要藉由觀察來擬訂接下來要改哪些東西。

其實就跟前兩天爆爆王遊戲修正 bug 的流程是一樣的,都是先執行出來看有什麼問題,但我當時主要想談的是:藉由多次的實際體驗來抓到錯誤,並進行多輪快速的修復,也因此可以修正到一些特定情境才會發現的 bug,但當時沒有詳細說明我在體驗過程當中是如何發現哪裡有問題的。

所以今天想來談談如何從一次的執行當中,藉由觀察分析出可能的錯誤在哪邊。那我們就開始分析吧!

分析錯誤

選擇並進入遊戲後出現的第一個畫面

首先很明顯的是這裡還殘留選單的畫面:

https://ithelp.ithome.com.tw/upload/images/20261004/20182439egV1uMRO6a.png

接著是中間被截斷的遊戲畫面:

https://ithelp.ithome.com.tw/upload/images/20261004/201824399Tz4tG1znz.png

這時我們試著用自己的話寫下觀察到了什麼:

  • 觀察:印出的順序是 選單 → 推箱子 → 選單,而且每一次畫面都是位移、疊加。

那麼我們注意到哪邊跟理想的狀態不一樣:

  • 和理想的差異:順序應該是 選單 → 推箱子,不應該再印一次選單,而且畫面不應該同時有選單又有遊戲。

所以問題在哪裡:

  1. 選單多印了一次
  2. 畫面應該清除重新繪製

開始遊玩之後的畫面

https://ithelp.ithome.com.tw/upload/images/20261004/2018243922ROzcwA7Q.png

從上面這張圖,再試著用自己的話寫下觀察與分析:

我們發現當我們開始玩遊戲的時候,新的遊戲畫面會覆蓋舊的遊戲畫面,表示我們原本推箱子重繪的邏輯還存在,這是好事。另外一個問題是現在當遊戲結束時,會直接離開整個程序,這就不是重繪的問題,而是我們監聽器需要調整的部分。

問題彙整

所以根據上面的觀察,整理出問題大概在這三個地方:

  1. 重繪不乾淨
  2. 選單多渲染了一次
  3. 獲勝之後,需要暫停在該畫面,使用者可以按 Q 或 q 離開遊戲,也可以按 ctrl + C 離開整個好玩遊戲區。

修正重繪與渲染順序

知道問題在哪裡的話就可以來看程式碼抓漏一下,先來找到關鍵的時間點,錯誤是發生在按下 enter 之後,所以來看看 enter:

    // Enter
    if (key === '\r' || key === '\n') {
        // 進入選擇的遊戲
        const game = games.find(game => game.id === focus);
        guideline = `你選擇的是 ${game.name}`;
        game.script();
    }
    
    // 中間省略
    
    render();

那我們可以看到這裡的兩個問題:

  1. 遊戲會直接在當前游標印出來,在選擇以前,我們的游標在選單最下面(如圖),所以這時候當我們直接在此執行 script 就會在這裡往下印出,所以我們要在執行遊戲之前將游標移動回左上角 → 清空畫面 → 執行 script 把遊戲叫出來。

https://ithelp.ithome.com.tw/upload/images/20261004/20182439992xwOKJbq.png

  1. 另外一個是每次監聽到有按鈕傳入之後,都一定會接到 render 去做重新渲染,這也就是導致我們的選單多印一次蓋住遊戲畫面的原因。

針對這兩個問題,讓我們把程式碼修正成這樣:

    // Enter
    if (key === '\r' || key === '\n') {
        // 進入選擇的遊戲
        const game = games.find(game => game.id === focus);
        process.stdout.write(`\x1b${height}A`); // 游標回到左上角
        process.stdout.write('\x1b[J'); // 清除畫面
        game.script(); // 執行遊戲
        return; // 結束這個監聽流程,不要往下跑到 render
    }

試著玩看看

2

這時我們發現還是有殘影,是出現在開啟遊戲之後,按下了第一步,此時就會渲染出選單,這時我發現只在 enter 的時候避免掉 render 是不夠的,因為每次按下按鍵,但是遊戲區入口的監聽和遊戲本身的監聽都還在運作,就會導致遊戲入口的監聽還是會每次都印出選單。

沒在用的監聽器不要動

所以現在監聽動作的部分,我們還有兩個要調整的:

  1. 當使用者在選單的時候,遊戲監聽不運作,當使用者在遊戲中的時候,選單監聽不運作。
  2. 獲勝之後,需要暫停在該畫面,使用者可以按 Q 或 q 返回選單,也可以按 ctrl + C 離開整個好玩遊戲區。

我想先優先來處理第一項,但是這涉及到我對於 process.stdin.on 這個方法還不是很熟悉,每到了這個時候,第一個反應都很想要請出 AI 來幫忙啊…,但其實我有想到一個想法,所以我想再堅持一下。

這個想法就是我何不設計一個變數來記錄玩家的狀態是在哪一個遊戲或者在入口:

let playerState = 0; // 用 0 當作是入口

然後直接在監聽到有按鍵輸入的時候,如果不是入口,就直接 return,這樣就不會有選單再次被渲染的問題了!

process.stdin.on('data', (key) => {

    // 如果不是選單就直接終止後面的監聽
    if (playerState !== 0) {
        return;
    }

    //其他內容不變

    // Enter
    if (key === '\r' || key === '\n') {
        // 進入選擇的遊戲
        const game = games.find(game => game.id === focus);
        playerState = game.id; // 這裡在選擇遊戲後改成遊戲 id
        process.stdout.write(`\x1b${height}A`);
        process.stdout.write('\x1b[J');
        game.script();
        return;
    }

    // 後面內容不變
});

到這邊我們先驗證看看有沒有成功轉換過去:

3

耶,這樣就成功了,但是我們要接著去處理兩個遊戲內的監聽,要讓狀態可以從遊戲轉回到入口,並避免掉獲勝或失敗直接結束遊戲。

從遊戲回到選單的方法

由於我們剛剛是用了 playerState 這個變數來決定要不要啟用監聽器,可是遊戲內不會有這個變數,所以我們要寫個方法傳給遊戲,讓遊戲端可以藉由這個方法把 playerState 和畫面切換回來。

function backToMenu() {
    playerState = 0; // 設回選單
    render(); // 重新渲染
}

// Enter
    if (key === '\r' || key === '\n') {
        // 進入選擇的遊戲
        const game = games.find(game => game.id === focus);
        playerState = game.id;
        process.stdout.write(`\x1b${height}A`);
        process.stdout.write('\x1b[J');
        game.script(backToMenu); // 這裡要傳入剛剛寫好的函式
        return;
    }

另外要去修改遊戲把這個函式傳進去:

export function startBox(backToMenu) {

    //前面都不動
    
        // 如果按下字母 'q' 或是 'Q',就回到選單
        if (key === 'q' || key === 'Q') {
            process.stdout.write(`\x1b[${map.length}A`); // 這邊兩句先清除遊戲畫面
            process.stdout.write('\x1b[J');
            backToMenu(); // 要回到選單
            return;
        }
        
     // 後面也不動

}

4

結果遊玩時發現遊戲的部分還是沒停掉,這時候我覺得我土法煉鋼去阻擋 return 似乎不是一個好方法,總有辦法可以直接停掉遊戲吧?但就如同我前面說的,我對於這個技術不那麼熟悉,當我卡住的時候,還是決定來用一下 AI。

要不要用 AI 在這個系列中判斷的標準:會卡住很久的,就用 AI。如果自己還有想法、知道怎麼做,就試著做看看。

在看 AI 實作的結果以前,先來看一下它給的總結:

https://ithelp.ithome.com.tw/upload/images/20261004/20182439EwnsmSRFoD.png

除了我們一直關注的監聽之外,它還另外抓到了計時器的部分也要做清除,總之我們先來玩看看。

5

我想帶大家再仔細的來觀察一次這個版本,你可以比較在按下 q 回到選單之前的遊戲畫面,以及從選單再次進到同一個遊戲的遊戲畫面。

進到選單之前:

https://ithelp.ithome.com.tw/upload/images/20261004/20182439eT98t4ATmh.png

進到選單之後再重新回到遊戲:

https://ithelp.ithome.com.tw/upload/images/20261004/20182439Cwz15lHL7m.png

你有注意到什麼問題嗎?箱子的位置都沒有改變、敵人的動作是延續的、玩家的位置也是延續的,這代表整個遊戲是暫停 → 進到選單 → 回到遊戲後恢復,這讓我突然意識到我們從頭到尾沒有去定義過我們期待按 q 回到選單的時候,到底是要暫停還是要結束遊戲,下次重新開始。

另外程式碼為了暫停而做了蠻多的妥協是我不太喜歡的,比方說在這些地方:

https://ithelp.ithome.com.tw/upload/images/20261004/20182439PYTRm1cxXQ.png

https://ithelp.ithome.com.tw/upload/images/20261004/20182439wjXrHmTzsP.png

為了要讓炸彈的時間暫停下來,多了 pendingBombTimers 刪除、新增的邏輯,我覺得這些把遊戲邏輯複雜化了。

如果我不喜歡這樣的寫法,那麼在這裡先停下來,釐清我的期待是什麼,重新訂定架構的方向似乎是更重要的事。

明天我們就來想想有沒有更根本而且更容易的解決方式吧!

如果你想要瀏覽完整的程式碼,可以參考這裡:第 21 天程式碼


上一篇
Day 20 共用入口1:從承認不知道開始的經驗值提升之旅!
系列文
重新拿回思考力!一起來用 JavaScript 打造 CLI 好玩遊戲區! 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言