iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

完賽:一個 QA 花 30 天做出來的東西

🎮 https://lucy-zhang-odo.github.io/ironman-js-battle/

不用安裝、手機也能玩。原始碼在 GitHub。

30 天前我一行 JavaScript 都沒寫過。

這個遊戲現在有什麼

  • 回合制戰鬥:一次點擊 = 你打牠一下、牠打你一下
  • 傷害浮動:同樣的攻擊力,結果不保證一樣
  • 骰子系統:六個面,奇數幫你、偶數幫敵人,三回合冷卻
  • 增益與減益:蓄力、治療、破甲、狂暴、再生、硬化
  • 受擊震動、血條變色、會累積的戰鬥紀錄
  • 最少回合紀錄,存在瀏覽器裡
  • 自訂名字、勝敗結果畫面、手機版排版、無障礙設定
  • 兩個彩蛋(等一下揭曉)

30 天學會的東西

階段 內容
Day 1–12 變數、型別、字串、判斷、陣列、物件、迴圈、函式、作用域、DOM、事件
Day 13–21 XSS 與轉義、CSS 操作、表單驗證、亂數、計時器與事件迴圈、狀態管理、非同步、localStorage、除錯
Day 22–30 專案結構、戰鬥系統、數值平衡、CSS 動畫、增益系統、RWD、部署

但真正留下來的不是語法清單,是幾個反覆出現的觀念:

  • 安靜失敗最危險 —— 30 天累積了十幾個「不報錯但也沒生效」的例子
  • 同一個值不要散落在很多地方 —— 骰子數值、CSS 顏色、遊戲規則
  • 基礎資料和算出來的結果要分開 —— 增益、顯示層、有效值
  • 一個 bug 要能被發現,前提是有人知道正確行為是什麼

🥚 揭曉:我藏了兩個彩蛋

兩個入口都在同一個地方:名字輸入框。

整個系列裡,那個框看起來只是拿來取名字的。實際上它是兩個秘密的鑰匙。

彩蛋一:逆位 —— 從魔王的視角戰鬥

把名字輸入成 魔王(或 BOSS、大魔王、魔王大人),然後出征。

畫面會整個沉下去:暗色主題、標題變成「魔王 vs 勇者 —— 逆位」、血條從綠紅變成紫紅與冷藍,戰鬥紀錄冒出一行螢光綠的字:

……有人在呼喚黑暗。

然後你就是魔王,勇者來討伐你。

而且數值也對調了 —— 你變成血少(60)但攻高(15)的那一方,對手是血厚(100)但攻低(9)的討伐者。這比正常模式難,因為你的容錯空間小得多。紀錄也存在不同的地方,不會污染正常模式的成績。

這個想法是我在 Day 22 規劃最終作品時冒出來的:整個系列都在打小怪,如果有人發現自己也能當魔王,應該會很有趣。

彩蛋二:名字可以自訂樣式

這個的來歷比較特別 —— 它原本是一個真的資安漏洞。

Day 15 我加上名字輸入框之後,自己測出一個問題:在名字裡打 <b>hl</b>,戰鬥訊息裡的 hl 真的變成了粗體。標籤被解析了,那就是 XSS。

當時我用轉義把它堵起來,並且在文章最後留了一句:

這個漏洞其實還有另一種處理方式,不是把它堵起來,而是把它變成一個功能。我留到系列最後一天再說。

就是現在。試試看輸入:

[red]小明[/red]
[b]勇者[/b]
[blue]水[/blue][red]火[/red]

名字會在血條旁、戰鬥紀錄、結果畫面三個地方都套用樣式。

但它怎麼可能是安全的?

因為順序:先把所有真的 HTML 轉義掉,再把「我自己定義的標記」還原成標籤。

function styleName(raw) {
  let s = escapeHtml(raw);                       // 第一步:攻擊在這裡就全死了
  for (const mark in NAME_TAGS) {
    s = s.replaceAll(mark, NAME_TAGS[mark]);     // 第二步:只還原白名單裡的
  }
  return s;
}

這叫白名單:不是「禁止我想得到的壞東西」,而是「只允許我列出的東西」。黑名單永遠會漏,白名單只會過於嚴格 —— 而過於嚴格是可以接受的。

所以輸入 <img src=x onerror="alert(1)"> 還是會原樣顯示成一串文字,一點事都不會發生;但 [red] 會變成紅色。玩家拿不到我沒給的東西。

這是論壇用了二十幾年的老方法。而我在 Day 15 把它記成待辦事項的時候,只知道「有這種做法」,今天才真的做出來。

一個小細節:標記不算字數

名字限制 10 個字,但 [red]小明[/red] 有 13 個字元。所以算長度時要先把標記拿掉:

function plainName(raw) { ... }   // 去掉標記,只算實際的名字

不然玩家加個顏色就沒名字可以取了。這種「功能和限制互相打架」的地方,不自己玩一次不會發現。


做彩蛋像做了一次壓力測試

這是我沒預期到的收穫。

兩個彩蛋加起來,逼出了五個 bug,而且它們全部是同一個家族:

# 哪裡 症狀
1 標題寫死在 HTML 逆位模式標題還是「勇者 vs 小怪」
2 骰子說明寫死「小怪」 逆位模式下訊息說錯名字
3 按鈕 hover 底色寫死淺色 暗色模式下按鈕整顆反白
4 只設了背景色,沒設文字色 暗色模式下輸入框的字看不見
5 設定裡寫死的名字蓋掉玩家輸入 樣式標記被丟掉

全部都是「在原本的前提下正確,前提一變就出錯」。

第 4 個特別微妙 —— 它不是「寫死了值」,而是漏掉了一半:背景和文字顏色必須成對設定,只改一邊,在另一種主題下就會壞。

而這讓我想起 Day 15 那個我最在意的發現:當時我在程式裡留了一行註解寫著「這裡沒有使用者輸入,所以用 innerHTML 是安全的」—— 那句話在寫下的當天是真的,加了輸入框之後就變成謊言了。

寫死的值、過期的註解、只改一半的設定 —— 都是同一件事:假設會過期,但程式碼不會自己提醒你。

彩蛋逼我換一套前提跑一次,那些假設就全部現形了。我原本以為彩蛋只是好玩,結果它是這 30 天裡最有效的一次測試。


下一步:勇闖魔王城

這個遊戲的完整構想其實比現在大得多。當初我想做的是:

  • 十關,每關結束有 2~3 條路線可選(休息、戰鬥、寶物、冒險)
  • 不管怎麼走,至少會經歷五場戰鬥才會到 Boss 關
  • 道具、buff、debuff、事件
  • 玩家可以畫自己的勇者、小怪、魔王長相
  • 雲端英雄榜

三十天做不完,所以砍成了「一場戰鬥」。但砍掉的東西我都記著,而且這 30 天累積的東西剛好是它的地基:

現在有的 未來要擴充成
一隻小怪 十關不同的敵人
骰子六面 道具、技能、裝備
增益清單 完整的 buff / debuff 系統
本機紀錄 雲端英雄榜(需要後端)
單場戰鬥 分支路線的關卡流程

Day 27 做增益系統的時候我就寫過:「後續的怪物會需要不同的數值,勇者也需要不同裝備,那時候就不能再靠手算兩個數字了。」那是下一個階段的功課。

而雲端英雄榜那部分,Day 20 已經先想清楚了:分數不能存在瀏覽器,要存在伺服器,而且伺服器要自己驗算 —— 因為前端的一切都可以被繞過。

完賽感想:沒有那麼熱血

照慣例,最後一篇應該寫一段熱血的話。但我想寫實際發生的事。

這個主題是我自己有興趣、自己想做的。可是邊上班邊做,大概做到第三天(內容做到 Day 16 左右)就開始有點煩躁了。

不是因為題目無聊,而是因為它和「上班」之間的關係:明天還要上班,但今天還要維持更新。有時候晚上睡前才突然想起來這件事,然後手忙腳亂。

會撐下來,有三個原因:

第一,存稿。 我不是每天現寫。有幾個週末一口氣做了好幾天的份量,所以平日只要按發表。如果沒有這件事,我應該撐不到第十天。

第二,題目看得見成果。 選遊戲不是因為它簡單,是因為每天結束的時候畫面上會多一個東西。Day 10 第一次讓數字出現在網頁上、Day 25 第一次看到血條震動 —— 那些瞬間是真的會讓人想繼續。

第三,我是為了薪水開始的。 Day 29 寫過,這裡再說一次:起點是現實的。而現實的理由在你煩躁的時候反而比較耐用,因為它不需要你保持熱情。

所以如果要對一樣在考慮參賽的人說一句話,大概是:別高估自己的熱情,也別低估存稿的重要性。


三十天後

我還是一名 QA。薪水沒有變,工作沒有變。

但有些東西不一樣了:我看得懂錯誤訊息在說什麼、我知道「沒有紅字不等於沒問題」、我會為了驗證一個數值設計去寫一個跑 1000 場的模擬器,而不是手動測 20 次。

還有一個網址,裡面有一個我從零做出來的遊戲。

它很小。但它是我的。

去玩玩看吧 —— 記得試試看把名字取成「魔王」。

🎮 https://lucy-zhang-odo.github.io/ironman-js-battle/

感謝每一位讀到這裡的人。明年見。


上一篇
Day29|上線之後,我的紀錄不見了
系列文
QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言