經典練習:1–100 隨機猜數字遊戲 之 JavaScript 初學者心路歷程

Ya-Wen Hsu
8 min readAug 24, 2020
Photo by freestocks on Unsplash

花了一個禮拜天的下午,思考這個傳說中的經典練習該如何突破。

設計隨機猜數字遊戲:從 1–100 中,隨機猜一個數字,並且限定在 10 次內可以猜中。

先從「從 1–100 中,隨機猜一個數字」開始做吧!這看起來要用迴圈 Loop吧!一開始對 while 要怎麼寫傻住了,畢竟跟他還不是很熟悉。一步步來,再先從定義變數吧,總覺得寫程式凡事都會先來定義變數。定義 answer 答案 賦與它一個隨機數字值,再定義 guessAnswer 猜的數字,既然是猜數字,那也是個隨機數字,就和 answer 一樣的公式,這沒問題吧!

要進入while 迴圈大魔王了!我目前的等級,我常把迴圈和 if/else 搞混,但我還是先在腦中的想法寫下來,再來釐清修正錯誤。

這一切都看起來很合理,但跑程式後發現有些地方好像怪怪的。

  1. 為什麼沒有印出「猜中了」?
  2. 為什麼有時候沒有顯示出任何字?程式有在跑嗎?是因為迴圈裡沒有寫break 嗎?

滿負似懂非懂的疑惑,先回過頭來研究 while 的原理。假設 answer 是 30;guessAnswer 是 50 ,while 條件是 30 < 50 ,進入 while 裡會顯示「太大了」。往下跑到第 8 行,guessAnswer 換新數字後,再回到第 1 行 while 裡判斷比大小。

如果 answer 一開始就比 guessAnswer 數字還大的話呢?它不會進入while 迴圈耶!所以有時會顯示字,有時沒有顯示字,不是程式沒有跑,是沒進入迴圈裡。

該如何每次都進入 while 裡判斷,直到猜中為止再跳出來呢?

  • answer === guessAnswer 的時候?但這樣也不會進入迴圈,就會繞過 while 跳到最下面啦!不會天真的進到迴圈裡第 5 行的 else。如果第一次沒有猜中答案,就永遠沒機會再猜其它數字。
  • 當 answer 比 guessAnswer 還大或是 answer 比 guessAnswer 還要小的時候,進入迴圈裡判斷?畢竟猜中了就不需要去判斷比大小了。

那條件要怎麼表示 比 guessAnswer 大或是比 guessAnswer 還小?這就是不相等!== !所以將條件換成 answer !== guessAnswer 就可以了嗎?為什麼「猜中了」還是沒有被印出來?

我使用 console.log 去檢查迴圈之外的 guessAnswer 和 answer,相比較真的是相同的,但為什麼沒有顯示出猜中了?難道要把猜中了移出迴圈外?

因為如果相等,根本不會跑進迴圈內判斷呀!而顯示猜中了這條程式碼,是寫在迴圈裡面,所以即便猜中了,他也不會顯示出「猜中了」。要注意 while 條件式的設定是什麼。

突破盲腸後,修正程式碼把「猜中了」移到 while 迴圈外面

在寫這個筆記的過程,我發現當初想說一定要用到 else if 加條件來篩出 answer 小於 guessAnswer,其實不見得一定要這樣,原本認為使用 else不能加入條件,這樣會出現 answer 與 guessAnswer 相等 或 answer 小於guessAnswer,所以不能用 else。後來想想,如果相等,根本不會進入 while迴圈,所以 else if +條件,是可以放心改用 else 的。

再加上回合變數,計算共跑了幾次才猜中。再修正一下顯示的訊息,加入現在所猜的數字及猜中數字與答案比較,更可以幫助我確認程式有正確在跑我想要的跑法。

改成 function() 的寫法,將 answer 和 guessAnswer 重覆到的值變成一個function(),程式碼又可以縮短一點。

再來要研究限制在十次以內猜中這個條件了!

猜數字要如何限制在十次內?

這是由答案去套題目嗎?

這真的好難想啊!在思考的過程只有不斷去想要如何優化,要怎樣才能再猜更少次呢?

首先,範圍原本是每一次都從 1到 100 去隨機猜一個數字,直到猜中為止,這範圍都一樣大,每次猜中的機率都是一樣多,所以這一定是花最多次數去猜到數字的方法。

接著,我想到 Harvard CS50 其中最有印象「撕電話簿找名字」的例子,可從一本厚厚的電話簿,經過改良讓找到名字時間次數縮短。那我可以把猜到的數字跟答案比大小後,將猜測範圍重新定義。例如:若猜的數字比答案大的話,那範圍調整成從上次範圍的最小數字到這次猜的數字之間,去猜新的數字。每猜過一次數字,範圍就會縮小,再怎麼樣一定都比每次範圍固定 1到 100 ,還要快猜中答案吧!

假設猜的數字是 40,答案是 20,下次猜數字的範圍就從 1 到 100,縮小為 1到 40 去猜。

也就是說,範圍最大值和最小值在每次猜測若不正確,最大值或最小值其中一個值,會跟據猜測的數字做調整。新的範圍會再縮小並重新定義。

把這個想法改進程式後,的確次數被縮短很多!但我發現還是有超過十次,甚至有快到二十次才猜到答案,為什麼會這樣呢?於是我把每一次猜測數字及新範圍列印出來去檢查,發現即便有縮小範圍,電腦還是很傻的會猜到重複的數字,害得猜測次數變多!這可以改進的吧!

要如何避免猜到重複的數字?

範圍定義要避開原本的數字範圍,那就是 min+1、max-1,這樣新範圍就會扣除原本的數字了!

再測試後,發現雖然又減少了次數,但還是有超過十次的時候啊!縮小範圍到剩五個數字了,居然要再猜三次才猜到,這運氣也太差了吧!

9 turn(s). Your guessing number is 79 too large! 74 78 
10 turn(s). Your guessing number is 75 too small! 76 78
11 turn(s). Your guessing number is 76 too small! 77 78
12 turn(s). Your guessing number is 77, and the answer is 77. You win!

範圍要如何再更精簡?還是有其它方法?規定猜測次數嗎?超過十次就也沒用啊~沒猜到就是沒猜到。應該還是得專注在如何讓範圍更為精簡,來幫助電腦可以更快猜到答案。

可以拿答案去跟新範圍比大小嗎?

例如:猜數字 71,答案是 39,新範圍是 1 到 70,拿答案 39 再去跟 1 到 50 範圍比較,如果答案 39 在這個範圍內,那新範圍改成 1 到 50,而不是 1 到70 ; 反之若答案不在這範圍內,那新範圍就是 50 到 70。

這樣是作弊嗎?

還是一開始就先切割兩個範圍:1–50 和 51–100 ?先從1–50 開始猜?或是一開始就指定猜 50?

假設 答案是 39,先猜 50。50 比 39 大,那新範圍就變成 1–49。我把程式中 guessAnswer 變數最 一開始賦值 50 let guessAnswer = 50。程式測試好多次,發現都可以在十次以內找到數字!最多是八次!也就是我在第一步就先把範圍直接縮小為一半。

同理!!!如果每次都是讓範圍減多一點,像是每次都減一半。

如果正確答案是 1,先猜 50,再猜 25,再猜 14,再猜 7,再猜 3,再猜 1,共花了 6 次猜到答案。再回頭思考,在最壞的情形下會需要多少次才會猜到答案?

範圍 1–100,最壞要猜 100 次才會猜到答案。

要如何確定可以在十次以內?如果不是用套答案,要怎麼知道把範圍每次都減一半,可以在十次內找到答案?

就是看 100 為 2 的幾次方?2x2x2x2x2x2x2=128 …突然開根號離我好遙遠,幾次方就是最多需要幾次才會得到答案!

應該要再複習一次 Harvard CS50 精彩的撕電話簿照答案示範,這方法叫作二分法 Binary Search。

整個範圍去二分法猜數字,呼應 Harvard 撕電話簿的方式,每一次都指定猜新範圍的一半數字:第一次是猜 50,與答案比大小後,第二次是猜 25 或猜75,如果比答案大,就往 50–100 中一半猜 75。

嘗試找出規律:第一次猜 50,該如何算出若 50 比答案大第二次要猜 75,若50 比答案小的第二次要猜 25 呢?

(最大值-最小值)除以 2,再加上最小值。要注意有時候除以二會有餘數,所以要用 Math.floor()。新增一個 function() 去計算,每次要猜數字的值都是新範圍的一半: function middleNumber(min, max) { return Math.floor((max-min) / 2) + min }

這裡發現一個奇怪的地方,為什麼第一次是 25 不是 50 呢?原來我 while loop 裡第16行 guessAnswer 放到 console.log 上面行了,所以顯示新猜的數字,應該要放在 console.log 下面行。

我再把一開始的程式中定義 guessAnswer 直接指定為 function middleNumber,因為在最一開始就指定好 min 是 1,max 是 100,所以最一開始的 guessAnswer 就一定是 50 了,可以直接叫 middleNumber 函數。

但跑好幾次程式,覺得奇怪最多都在十次內完成,為什麼不是之前算的七次呢?而且看起來每次猜的數字都不是照 middleNumber 給的算式跑啊?

原來是在 while loop 裡最後第 25 行還留著 guessAnswer = randomNumber(min, max) 重新定義了 guessAnswer。那次數比較少或控制在十次以內,只是因為第一次指定 50,猜到答案的次數一定會在十次以內。

另外再把一開始答案顯示出來做檢查用的程式碼刪除。更正之後,最多都在七次完成了!

就這樣我一個禮拜天在解這題的過程中結束了。這真的是滿滿初學者學JavaScript 的心路歷程,也是我第一篇發佈長篇文章。認真看到這行的你,在此祝福你一切順心。如果我哪個步驟或思考還能再更好,請不吝惜指教。

最後程式如下:

--

--

Ya-Wen Hsu

Remote worker. Stationery enthusiast. Know how to play the piano. Self-taught. Exploring the coding world.