邊緣人的終極密碼 —JavaScript 程式入門經典練習題「猜數字猜到飽」

這幾天在寫 JavaScript 的程式入門經典練習題 —「猜數字」,概念有點像是小時候會和朋友一起玩的「終極密碼」,不過印象中大家都是避免猜到指定數字,因為誰也不想「中獎後」自己變成大家的禮物 — 表演助興娛樂眾人。另外,過往玩終極密碼時,大家總得輪流當指定數字的莊家負責主持,如果會寫這支小遊戲那就沒有這問題了,所有計算與主持都可以交給程式,回回每個人都能同樂。But... 本人還無法生出前端介面讓用戶輸入數字,目前只練習到讓「電腦邊緣人自己猜數字」而且已覺得小有難度,還有非 ~ 常~ 遠的路要走阿。(握拳)


玩終極密碼時,大致上可分為兩種類型的玩家。一是天真派的佛系玩家,對於莊家提示都當耳邊風,只隨自己心意隨意喊數,不會細想或有更多安排。第二種則是小聰明玩家,運用莊家給的資訊,快速縮小猜數範圍,並一步步「幫助其他玩家」步入陷阱之中。(喂)

我們的練習也分成兩個階段,本次先記錄「佛系玩家」的練習,已將 AC 助教和同學針對我的程式碼之回饋整理如下:
  1. 第 4 行已宣告變數 guess = 0,while 迴圈內的 let guess 就不用再次宣告了,賦值即可。
  2. 第 14 行的 while 迴圈只在 guess 不等於 answer 的情況下執行,也就是 guess 等於 answer 時(第 20 行的 IF 條件式成立時)迴圈就會自動結束,而且外層也已宣告過 guess 因此不需要第 22 行的 break 來中斷迴圈。→ 之前一直搞不懂為何 break 和外層宣告的 guess 有關?文章寫著想著,過了很久以後終於想通!(到底為何不用 break?👉 筆記傳送門
  3. 因為 PM 這個職務總是得要想很多 我習慣把盤到的所有狀況都列出來(第 25 行的 else if),不過 RD 通常在開發時最後一個會直接寫 else。



接著是寫作業時產生的一些碎碎念筆記。

猜數字遊戲需求描述
  1. 莊家事先指定介於 1-100 之間的數字(這裡由邊緣人電腦隨機指定 XD)
  2. 佛系玩家們輪流開始猜數字(這裡繼續由邊緣人電腦一機分飾多角 XD)
  3. 玩家隨意喊出數字以後,莊家回答「太小了」或「太大了」。
  4. 直到玩家猜中,遊戲結束。

Pseudocode and Flowchart



開始寫程式!




Step 1. 設計變數
const answer = Math.floor(Math.random() * 100) + 1
let guess = Math.floor(Math.random() * 100) + 1
let count = 0

其實本人最初只在外層(Global)宣告了 answer 後就往下寫迴圈了,完全忘記還需要宣告「猜的數字」。

const answer = Math.floor(Math.random() * 100) + 1

//

我們在記憶體宣告一個常數「answer」(標記起來的空間),並在記憶體創建一筆「資料」(而這筆資料會是一個從 1-100 隨機產生的一個整數),常數「answer」指向這筆資料,而「answer」這個常數(空間)不能再指向(裝入)其他資料!

//

翻譯蒟蒻 → 莊家從 1-100 隨機喊一個整數做為本局猜數字遊戲的指定數字,而這裡我們使用函式讓電腦隨機出數。

宣告 answer 時,我最初是使用 let(變數),但整理本文時忽然想到「answer」(莊家指定數字)是不會變動的,因此這裡使用 const(常數)的設計可能更好?而 answer 的值包含兩個函式,由 Math.floor() 在外層包著 Math.random()。
  • 先看內層的 Math.random()*100 
    • Math.random() 是隨機指派數字「0 ~ 0.999...」的函式。
    • 由於我們最多會喊到數字 100,因此我們將函式乘以 100,讓此函式指派的數字變為「0 ~ 99.9...」。
額,可我們猜數字只會猜整數,應該沒有人喊 85.7 這種數字吧我的祖奶奶?

  • 別急,這時候就需要外層的 Math.floor() + 1 來幫忙了!

    • Math.floor() 回傳數值時,若數字不是整數的話,這函式就會直接把小數點後方的值給去掉。例如:Math.floor(99.9), Math.floor(99.97), Math.floor(99.976) → 不管 99 的小數點有幾位,回傳的數字都會被無條件捨去,只回傳 99 這個數字而已。

    • 題外話:floor 這個英文字除了有我們常見的 名詞用法「地板」外,口語上也有「把...打倒在地」之意。Math.floor() 就像是個護花使者,會把「尾隨在公主後方的豬哥都打倒在地」,然後安然將公主護送回來的感覺哈哈。

    • 最後我們把豬哥小數點給打倒在地之後,送一個護衛給公主,讓他的防禦力增強(喂):Math.floor() + 1,於是回傳的值若是 99,就會自動 +1 變成 100,而回傳值若是 0,就會變成 1 了。終於....呼....
let guess = Math.floor(Math.random() * 100) + 1
let count = 0

//

我們在記憶體宣告一個變數「guess」(標記起來的空間),並在記憶體創建一筆「資料」(而這筆資料會是一個從 1-100 隨機產生的一個整數),變數「guess」指向這筆資料,而「guess」這個變數(空間)可以再指向(裝入)其他資料! 接著宣告變數「count」.......

//

翻譯蒟蒻 → 玩家從 1-100 隨機猜一個整數,而這裡我們使用函式讓電腦隨機出數。猜的次數起始值為第 0 次。

我最初沒有在外層設計 guess 變數,只有在接下來的 while 迴圈內宣告 guess,因此當執行到 while 迴圈的條件式 (guess !== answer) 要用到 guess 時,電腦就回傳 "ReferenceError: guess is not defined" 的錯誤訊息給我,因為「程式是由上而下執行」的,在 while 迴圈之前找不到 guess 就卡住了,根本就輪不到迴圈裡面的 guess 出場哈哈。


外層宣告的變數 guess,真的很影響下面的 while 迴圈。guess 除了讓 while 迴圈的條件式可以順利運作外,當條件式是 (guess !== answer) 的情況下,還能免除無限迴圈的情況發生。因為
  1. 「猜的數字 ≠ 指定數字」→ 迴圈條件成立 →  執行程式
  2. 「猜的數字 = 指定數字」→ 跳出迴圈 → 重新回到第一行程式碼檢查發現..... ↓
  3. 跳出迴圈後的 guess 已被賦予新值(也就是在最後一次迴圈:猜的數字 = 指定數字變,guess 已不再是最初在外層宣告的值了)→ 因此自然不會再有「猜的數字 ≠ 指定數字」這個情況,也就不會回到迴圈裡面,當然也就不需要有 break 來幫忙迴圈終止。
天啊好像有點饒口,不過終於想通外層 guess 和迴圈的關係了......到底為何這裡思維一直卡住😰

而 guess 這個變數不管值是什麼數字都無所謂,因為它無法被印出來做為我們其中一個猜數字的結果(不在迴圈內我們無法判斷這個數字是大或小,right?)

不死心地偷偷宣告 guess = 5 印出來看看..... XD

====================
★ 莊家指定數字 73 ★
====================
#0:computer guess 5

--- 猜數字實況 ---
#1:computer guess 7 |TOO SMALL!
#2:computer guess 35 |TOO SMALL!
#3:computer guess 95 |TOO BIG!
#4:computer guess 54 |TOO SMALL!
#5:computer guess 18 |TOO SMALL!
#6:computer guess 81 |TOO BIG!
#7:computer guess 81 |TOO BIG!

那 let count 是宣告「猜的次數」的變數,是要和迴圈內的 count++ 搭配使用的,這裡若不需在輸出畫面上呈現「這是第幾次的猜測結果」如「第 1 次 computer guess 27」,好像也不必設計這個變數。

但 let count 也不能和 count++ 一起放在迴圈裡,這樣迴圈在運行時,每次都會宣告一次 count 為零然後再加一,這樣就會變成每次執行的結果都是第 1 次。PM OS: 是在搞什麼東西.....連這種小遊戲都有蟲!(誤)


Step 2. 輸出莊家所指定的數字
console.log('====================')
console.log (`★ 莊家指定數字 ${answer} ★`)
console.log('====================')
console.log(' ')

Step 3. 輸出玩家猜數字實況
console.log('--- 猜數字實況 ---')
while (guess !== answer) {
  guess = Math.floor(Math.random() * 100) + 1 

  if (guess === answer) {
    console.log (`#${count}:computer guess ${guess} |WIN!👍👍👍👍`)
  } else if (guess > answer) {
    console.log (`#${count}:computer guess ${guess} |TOO BIG!`)
  } else if (guess < answer) {
    console.log (`#${count}:computer guess ${guess} |TOO SMALL!`)
  }
}

這裡主要想筆記前半部 while 迴圈的部分。

while (guess !== answer) {
guess = Math.floor(Math.random() * 100) + 1
count ++

//

翻譯蒟蒻 → 當(猜的數字 ≠ 指定數字時)玩家就{從 1-100 隨機猜一個整數,並且猜的次數 +1}
 
因為我們不知道「玩家要猜幾次」才會猜中,因此選「只要條件有效,就持續執行」的 while 迴圈(不能選要控制次數的 for 迴圈)。那我最直覺想到「只要玩家猜的數字,和莊家指定答案不相同」這個條件成立時,玩家就需要一直猜,前兩行便是在表達這個。

count ++ 則是猜的次數,每猜一次,猜的次數就 +1。因為我們希望在輸出畫面上能呈現「這是第幾次的猜測結果」,如「第 1 次 computer guess 27」,若沒寫 count++,迴圈執行時每經過一次,都只會抓最外層宣告的 count = 0 來使用,那我們每一次的結果都會變成......

====================
★ 莊家指定數字 27 ★
====================

--- 猜數字實況 ---
#0:computer guess 5
#0:computer guess 70 |TOO BIG!
#0:computer guess 64 |TOO BIG!
#0:computer guess 84 |TOO BIG!
#0:computer guess 7 |TOO SMALL!
#0:computer guess 79 |TOO BIG!
#0:computer guess 69 |TOO BIG!
#0:computer guess 71 |TOO BIG!
#0:computer guess 58 |TOO BIG!
#0:computer guess 19 |TOO SMALL!
#0:computer guess 27 |WIN!👍👍👍👍



while 迴圈的其他寫法

這次觀摩其他同學的作業時,看到很多不同的 while 條件式寫法。像是 while(true) while(answer> 0),這兩種都會製作出無限迴圈,因此必須搭配 break 來終止。不過助教建議 while 的條件,盡量不要使用會製作出無限迴圈的條件。

製作出無限迴圈是個很可怕的情境,通常我們對於較短的程式碼,使用無限迴圈有自信可以控制得宜。但是當程式碼龐大起來時,任何一個無限迴圈沒寫好,就可能造成麻煩的錯誤產生。因此,與其埋藏地雷給自己或是他人踩,不如不要製作,選擇可控制的迴圈,才是安全之道。

另一點是,在 while 的條件寫明白該迴圈的執行條件,可以幫助他人閱讀程式碼時,快速了解該迴圈的使用功能,加速理解程式碼背後的邏輯。

— by AC TA Victor

助教另外還提供了如下寫法,宣告一個變數,讓它來負責控制 while 的條件:

let isContinued = true
while (isContinued) {
  if (answer === computer) {
    isContinued = false
  }
}

最後後半部 while 迴圈中的 if 條件式本人認為還算單純,因此本篇筆記就記錄於此。居然寫了四千多字廢話一堆,然後我覺得自己沒有力氣看新進度了.....




留言

這個網誌中的熱門文章

如何在 Blogger 文章中顯示程式碼區塊?

排球少年山口忠:平凡的我們也能威力十足