第 18 章  範例

德嘉書業 / 德嘉資訊科技 (www.takka.com.hk) 作者: 伍新華 Email: ng-sun-wah@graduate.hku.hk

 

  很多讀者看完有關程式的書藉, 都會有一個埋怨, 就是學了一大堆語法, 只能寫出零星的片段程式, 較大型及完整的程式則不知從何入手, 原因是這些書藉都不會教讀者如何將這些片段合併及連貫來組成一個完整的大型程式。

  本書前面各章的資料只是基礎課程, 讓你明白 JavaScript 的語法, 所用的例子都是簡單的零星片段, 沒有實用性, 來到這處最後一章, 筆者示範如何合併這些基本操作來設計較大型的遊戲及學習程式。請你特別留意 18.4 一節的例子 (Hangman 遊戲), 看如何從 algorithm (程式進展的構思) 開始, 然後逐步加進程式片段, 組成一個輪廓後就試用, 有問題就修正, 經過 debug 就完成一個完整可用的遊戲。

  這一章有四個不同類型的例子, 第一個是簡單的雙人遊戲, 第二及第三個是輔助教學用的練習或遊戲, 第四個是一個賭博遊戲, 但筆者只是說明 JavaScript 的操作, 不理會美術設計, 在你實際將一個遊戲放在自己的網頁時, 還要留意美術及裝飾方面的工夫。

 

  另還要留意以下兩點可能一時大意的錯誤:

1. 遇到數目 l 字與英文字母的 1 字要特別小心, 例如使用 image1這變數 (這 1 是數目字), 若一時大意打了 imagel (這 1 是英文字母), 當程式不能操作, 用肉眼檢查原始檔案很難發現錯誤。

2. 輔助及圖片檔案名稱盡量全部用小寫, 例如你在網頁中放下這圖片名稱: <img scr=house.gif>, 而實際在硬碟的檔案名稱是 House.gif, 在你的個人電腦中測試, 不會出現問題, 但將檔案送上 Unix server, 這圖片不能顯示, 因 Unix 的檔案系統是分大小寫的。(若你在自己的電腦試驗程式或使用網頁時, 一切操作正常, 上網後不能操作, 多是這個原因。)

 

 

 

18.1 模組的編寫方式

  編寫較大型的程式, 有兩點要留意:

1. 最好能先寫好一個 algorithm (演算法), 這是用文字或流程圖的方式來描寫程式的進展, 請參看 6.2 的一節及 練習-122 的例子。

2. 盡量使用模組的編寫方式。


圖 18-1 模組的編寫方式

 

  模組方式是將一個程式分為多個獨立部份來編寫, 每部份稱為模組 (module), 寫完一個模組後, 就檢查這模組是否能操作, 有問題就只需針對這模組來研究, 不用理會其他模組, 這就可縮窄 debug (除錯) 的範圍, 每個模組寫好後, 就可將各模組合併成一個完整的程式。

  在 1.2 的一節說到如何設定跑動文字, 就是標準的模組方式, 每部份測試成功, 就編寫第二部份, 然後再將兩部份連接。

 

 

 

18.2 遊戲: 一分鐘時距有多久?

  當你看著時鐘時, 時間就變為實物化, 一分鐘有多久, 可以從時鐘看出來。但當你看不到時鐘, 是否能準確估計一分鐘時距 (interval) 有多久呢? 

  這一節說的是雙人遊戲, 甲方及乙方都不看時計, 然後分別判斷一分鐘時距, 最接近實際一分鐘的就勝出。

 

1. JavaScript 的浮點數誤差

  當使用 JavaScript 作計算時, 要留意浮點數的輕微誤差, 在準確性上問題不大, 但在顯示上就很礙眼, 請你試試以下的 script, 這計算有小數點的數字, 你可看看有什麼結果。

<html><body>
<script>
document.write(
62.28 - 60 )
</script> 
</body> </html>

  62.28 減 60 的正確答案是 2.28, 但在 IE-4/5 及 Netscape-4, 上述的 script 都會得出 2.280000000000001, 誤差實際很輕微, 不影響數字的準確性, 但顯示上卻有十多個小數位。

  若只要兩個小數位, 可以將得出來的數字乘 100, 用 Math.round( ) 以四捨五入的方式取消小數位, 再將數字除 100, 例如以下的寫法:

<html> <body>
<script>
x=Math.round( (
62.28 - 60 ) * 100 ) /100
document.write( x )
</script> 
</body> </html>

  以上的修改就會得出 0.28 的答案。

 

2. 初步程式測試

  構思一個遊戲時, 首先要考慮的是這程式語言是否有能力造出這遊戲, 又或是我們是否有足夠功力寫出這遊戲, 所以要寫出一個初步程式作為評估。

  請你開啟示範磁碟中的 time1.htm, 這網頁有以下內容:

<html> <head>
<script>
function startIt( )
{ dif=0 ; startTime="" ; endTime=""
  now=new Date( )
  startTime=now.getTime( )
}

function end( )
{ then=new Date( )
  endTime=then.getTime( )
 interval= (endTime - startTime) /
1000
 dif = Math.round( Math.abs(interval -
60) *100 ) /100
 alert("
你造出的時間是: " + interval + " 秒, \n \n"
    + "
比實際的一分鐘" + ( interval > 60?' ' : ' ') + dif + " 秒。")
}
</script> </head>

<body bgcolor=mintcream text=blueviolet>

<p> 請你先按 [開始] 的按鈕, 不要看任何計時器, 在心中估計一分鐘的時間, 時間到了就按 [結束] 的按鈕, 看你估計的一分鐘時距與實際的相差有多遠。 <p>
<form>
<input type=button value="
開始" onClick="startIt( )"> <br>
<input type=button value="
結束" onClick="end( )"> 
</form> </body> </html>

 

  請你試試這程式的操作, 留意這一句:

dif=Math.round( Math.abs(interval - 60) *100 ) /100

  這處利用 Math.abs( ) 取消負數, 所以 interval 少於 60, 也會變為正數, 乘 100 後用 Math.round( ) 取消多餘的小數位, 再除 100 就可將兩個小數位還原。

 

3. Algorithm

  試驗過程式可行後, 就可構思程式的佈局, 這佈局就是 algorithm, 筆者作以下的設計:

1. 網頁開啟後, 上方會說明可雙人玩, 也可以單人玩, 然後有簡單遊戲玩法的介紹。

2. 遊戲畫面有以下窗格畫分 (U 代表 upper, L 代表 left, R 代表 right):

3. frameL 及 frameR 有指示要遊戲者輸入甲方及乙方姓名及按 [開始], 若不輸入就用預設的"甲方"及"乙方"為名。

4. 甲方按 [開始] 的按鈕就開始計時, 在 frameL 會顯示 "夠一分鐘就按 [結束] "。

5. 乙方可隨時按 [開始] 按鈕, 按下這按鈕, frameR 也會顯示 "夠一分鐘就按 [結束] " , 甲方及乙方是獨立操作及獨立計時。

6. 按下 [開始] 按鈕後, 這按鈕就失效, 遊戲者只可按 [結束]。

7. 當一方先按 [結束] , 對方有按 [開始] 但未按 [結束] , frameL 或 frameR 會顯示 "請等待對方結束。" , 當對方按 [結束] , frameL 或 frameR 就會顯示請按 [顯示成績] 的指示, 按一下就會比較雙方成績, 看誰勝出, 結果在 frameL 及 frameR 顯示。

8. 若一方按 [結束] 時, 對方未有按 [開始] (即是單人玩), 會有指示要觀者按 [顯示成績] 的按鈕。

9. 按下 [開始] 後, 若某方按 [顯示成績], 會有對話盒提示遊戲者未按 [結束]。

10. 若未按 [開始], 按 [結束] 或 [顯示成績] 會有對話盒提示遊戲者要先按 [開始]。

11. 畫面下方有 [重新開局] 的按鈕, 按一下可清除已有的資料。

 

  在以上的設計, 這程式如何得知遊戲者是否已按下某個按鈕呢? 這就要使用 flag 的方法, 如下:

1. 程式開始時, 會設定兩個變數 : flagA=0 ; flagB=0
2. 甲方按 [開始] , 會將 flagA 變為 1, 按 [結束], flagA 變為 2。
3. 乙方按 [開始] , 會將 flagB 變為 1, 按 [結束], flagB 變為 2。

  例如在程式放下這檢查: if(flagA != 2), 若這是 TRUE, 表示甲方還未按 [結束], 在程式一路放下這類檢查, 就知遊戲者按了哪個按鈕。

  請你開啟示範磁碟中的 time3.htm, 這是完工的程式, 請你試試這遊戲的操作。

 

4. 設定窗格及主程式的網頁架構

  這遊戲有三個窗格, 所以首先要安裝好窗格及窗格內的網頁, 筆者將這主網頁命名為 time2.htm, 有以下內容: 

<html> <head>
<frameset rows=
75%,25% framespacing=0>
  <frame name="frameU" src="
ctrl_1.htm">
  <frameset cols=
50%,50% frameborder=1 framespacing=0>
    <frame name="frameL" src="javascript:document.write
      ('<html> <body bgcolor=oldlace text=navy> </body></html>')">
   <frame name="frameR" src="javascript:document.write
      ('<html><body bgcolor=oldlace text=navy> </body></html>')">
  </frameset>
</frameset>

<noframes>這遊戲使用窗格,你的瀏覽器不支援窗格操作。</noframes>
</head> <body> </body> </html>

 

  這程式的操作部份是放在一個名為 ctrl_1.htm 的檔案中, 在編寫網頁程式時, 我們一般習慣是先造出網頁的顯示架構, 程式的部份稍後才加上去, 在這開始階段, ctrl_1.htm 有以下內容:

<html> <head> 
<script>
writeL("指示: 請輸入甲方姓名, 然後按 [開始]。" )
writeR("指示: 請輸入乙方姓名, 然後按 [開始]。" )

function writeL(strL)
{ top.frameL.document.open( )
  top.frameL.document.write("<html><body bgcolor=oldlace"
   + "text=navy>" + strL + " </body> </html>")
  top.frameL.document.close( ) 
}

function writeR(strR)
{ top.frameR.document.open( )
  top.frameR.document.write("<html><body bgcolor=oldlace"
   + "text=navy>" + strR + " </body> </html>")
  top.frameR.document.close( ) 
}

</script> </head>

<body bgcolor=mintcream text=red>
這是雙人遊戲, 單人也可只玩甲方或乙方。當你按 [開始] 就會開
始計時, 請不要看任何計時器, 心中估計一分鐘時間, 時間到了就
按 [結束] , 看估計的一分鐘時距與實際相差多少, 相差少的一方
勝出。<p> <center>
<table width=
90% border=1 background=backgd.gif
cellspacing=
0 cellpadding=8> <tr>

<td align=center width=
40%
<form name=fmA>
甲方姓名: &nbsp;
<input type=text name=txA value=甲
size=10> <p>
<input type=button value="
開始" onClick="startA( )"> &nbsp;&nbsp; 
<input type=button value="
結束" onClick="endA( )">
</form> </td> 
<td align=center width=
40%> <form name=fmB> 乙方姓名: &nbsp;
<input type=text name=txB value=
乙方 size=10> <p>
<input type=button value="
開始" onClick="startB( )"> &nbsp;
&nbsp; <input type=button value="
結束" onClick="endB( )">
</form> </td> </tr> </table>

<form> <input type=button value="
顯示成績" onClick="result( )">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type=button value="重新開局" onClick="clearIt( )">
</form>

</body> </html>

 

  請你開啟示範磁碟中的 time2.htm, 就會見到這網頁的顯示架構, 各按鈕的 function 還未定義, 所以按下就會有錯誤的訊息。

 

function 的 argument 及 return:

  在以上的網頁, 筆者使用 writeL( )writeR( ) 這兩個 functions 來在 frameLframeR 顯示訊息, 請留意 argument 的使用, 例如: 

 

  當我們使用 writeL("指示: 請輸入甲方姓名, 然後按 [開始]。") 來叫用 writeL(strL) , strL 就會有以下變化:

strL = "指示: 請輸入甲方姓名, 然後按 [開始]。"

  因此在 top.frameL 中用 document.write( ) 來顯示  strL , 就會顯示 strL 代表的句子。

 

  在隨後說的 ctrl_2.htm 有以下的一個 function:

function getNameA(nameA)
{ nameA=document.fmA.txA.value; return nameA }

  這是用來讀取觀看者在文字框中輸入的姓名, 我們在別處放下類似 document.write(getNameA( ) ) alert(getNameA( ) ) 的指示, 就會顯示 document.fmA.txA.value 的文字。

  這 function getNameA(nameA) 不是絕對需要的, 筆者用這較複雜的方法, 只是示範如何用 argument 及 return 來讀取一個物件的數值。較簡單的方法是在 startA( ) 這 function 內使用 assignment 的方法, 例如: nameA=document.fmA.txA.value, 就可讀取到文字框內的資料, 這方法適用於不常變動的資料, 使用 function 的方法可以在顯示前才讀取資料, 適用於不斷更新的數值。

  有關 argument 及 return 的操作, 請看 練習-36 處的詳細解說。

 

5. 編寫遊戲程式部份

  有了上述的顯示架構, 就可開始編寫程式反應的部份, 這部份由觀看者按 [開始] 按鈕來啟動 startA( )startB( ) 開始, 請看以下的 script, 這是 ctrl_2.htm :

<html> <head> 
<script>
function clearIt( )
{ writeL("
指示: 請輸入甲方姓名, 然後按 [開始]。" )
  writeR("
指示: 請輸入乙方姓名, 然後按 [開始]。" )
  flagA=
0 ; flagB=0 ;
  nameA="" ; nameB="" ;
  difA=
0 ; difB=0 ;
  strL="" ; strR="" ;

clearIt( )

function getNameA(nameA)
 { nameA=document.fmA.txA.value; return nameA }
function getNameB(nameB)
 { nameB=document.fmB.txB.value ; return nameB }

 

function startA( )
{ if (flagA==
1) { return }
  difA=0
  flagA=
1
  nowA=new Date( )
  startTimeA=nowA.getTime( ) 
  writeL("
指示: 現已開始計時, 估計夠一分鐘就按 [結束]。") 
}

function endA( )
{ if(flagA==
0) { alert("請先按 [開始]。") ; return }
  flagA=
2
  thenA=new Date( )
  endTimeA=thenA.getTime( )
  intervalA= (endTimeA - startTimeA) /
1000
  difA=Math.round( Math.abs(intervalA -
60) *100 ) /100

 if (flagB==
0 || flagB==2)
    writeL("
請按 [顯示成績]。")

 if (flagB==
1)
    writeL("
請等待" + getNameB( ) + "按 [結束]。")
}

 

function startB( )
{ if (flagB==
1) { return }
  difB=
0
  flagB=
1
  nowB=new Date( )
  startTimeB=nowB.getTime( ) 
  writeR("
指示: 現已開始計時, 估計夠一分鐘就按 [結束]。") 
}

function endB( )
{ if(flagB==
0) { alert("請先按 [開始]。") ; return }
  flagB=
2
  thenB=new Date( )
  endTimeB=thenB.getTime( )
  intervalB= (endTimeB - startTimeB) /
1000
  difB=Math.round( Math.abs(intervalB -
60) *100 ) /100

  if (flagA==0 || flagA==2)
    writeR("
請按 [顯示成績]。")

  if (flagA==
1)
    writeR("
請等待" + getNameA( ) + "按 [結束]。")
}

 

function result( )
{ if (flagA==
0 && flagB==0) { alert("請按 [開始]。") ; return }

 if (flagA==
1 || flagB==1) { alert("有人還未按 [結束]。") ; return }

 if(flagA==
2 && flagB==0)
   { writeL("
你造出的時間是: " + intervalA + " 秒, \n \n"
      + "
比實際的一分鐘" + ( intervalA > 60? ' ' : ' ' ) + difA + " 秒。") } 

 if(flagB==
2 && flagA==0)
   { writeR("
你造出的時間是: " + intervalB + " 秒, \n \n"
      + "
比實際的一分鐘" + ( intervalB > 60? ' ' : ' ' ) + difB + " 秒。") } 

 if(flagB==
2 && flagA==2)
   { writeL("
你造出的時間是: "+ intervalA + " 秒, "
      + ( intervalA >
60? '多了 ' : '少了 ' ) + difA + " 秒。<br>"
      + getNameB( ) + "
造出的時間是: " + intervalB + " 秒, "
      + ( intervalB >
60? '多了 ' : '少了 ' ) + difB + " 秒。<p>"
      + (difA > difB? getNameB( ) : "
" ) + "勝出 !!! " )

     writeR("你造出的時間是: " + intervalB + " 秒, "
      + ( intervalB >
60? '多了 ' : '少了 ' ) + difB + " 秒。<br>"
      + getNameA( ) + "
造出的時間是: " + intervalA + " 秒, "
      + ( intervalA >
60? '多了 ' : '少了 ' ) + difA + " 秒。<p>"
      + (difA > difB? "
" : getNameA( ) ) + "勝出 !!! ") 
   }
}

function writeL(strL)
{ top.frameL.document.open( )
  top.frameL.document.write("<html><body bgcolor=oldlace "
   + "text=navy><font size=-1>" + strL + " </body> </html>")
  top.frameL.document.close( ) 
}

function writeR(strR)
{ top.frameR.document.open( )
  top.frameR.document.write("<html><body bgcolor=oldlace "
   + "text=navy><font size=-1>" + strR + " </body> </html>")
  top.frameR.document.close( ) 
}

</script> </head>

<body bgcolor=mintcream text=red>
這是雙人遊戲, 單人也可只玩甲方或乙方。當你按 [開始] 就會開
始計時, 請不要看任何計時器, 心中估計一分鐘時間, 時間到了就
按 [結束] , 看估計的一分鐘時距與實際相差多少, 相差少的一方
勝出。<p> <center> 
<table width=
90% border=1 background=backgd.gif cellspacing=0 cellpadding=8> <tr>
<td align=center width=
40%
<form name=fmA>
甲方姓名: &nbsp;
 <input type=text name=txA value=
甲方 size=10> <p>
 <input type=button value="
開始" onClick="startA( )"> &nbsp;&nbsp; 
 <input type=button value="
結束" onClick="endA( )"> 
</form> </td>
<td align=center width=
40%
<form name=fmB>
乙方姓名: &nbsp;
 <input type=text name=txB value=
乙方 size=10><p>
 <input type=button value="
開始" onClick="startB( )"> &nbsp;&nbsp; 
 <input type=button value="
結束" onClick="endB( )"> 
</form> </td> </tr> </table>

<form> 
<input type=button value="
顯示成績" onClick="result( )">&nbsp;&nbsp;&nbsp;&nbsp;
<input type=button value="
重新開局" onClick="clearIt( )">
</form> 

</body> </html>

 

  筆者將這完工的檔案以 ctrl_2.htm 的名稱存檔, 請你開啟示範磁碟中的 time3.htm, 在 frameU 就會載入這檔案, 請你試驗這程式的操作, 留意觀看者作出錯誤操作程序時 (例如未按 [開始] 就按 [結束] ), 這程式作出的反應。我們編寫程式時要小心處理這類使用者的操作錯誤, 處理得不好, 觀看者偶有偏差, 程式就不能繼續運作。

  這程式還有一點可以改善的, 若在遊戲中途, 觀看者按 [重新開局] 的按鈕, 應該有一個 confirm 對話盒問是否真你要重新開局, 這是預防觀看者一時大意按下這按鈕, 若在一局遊戲結束後按這按鈕, 則沒有這對話盒出現, 請你試試在這程式加進這 confirm 對話盒的機制。

 

 

 

18.3 字彙測驗程式

  這是一個英文字彙測驗 (vocabulary test), 因為這程式要進行一些文字檢驗, 讀者請重溫第 12 章 的一節, 這處不重複解說 String object 及 regular expression 的操作。

 

  請你開啟 vocab4.htm, 這是完工的網頁, 請你試試各項操作。因為這程式較多資料, 所以筆者不逐步解釋, 請你自行研究這程式的原始檔案, 或買《網頁程式-JavaScript》這書看看吧。

 

 

 

18.4 Hangman 遊戲

  這一節的例子較為複雜, 要用窗格來操作,觀看者一路估字時, 進度就一路在窗格顯示, 請你重溫 練習-128, 看如何將不斷更新的資料寫進一個記錄窗格。

  Hangman (負責絞型的劊子手) 是一個英文串字遊戲, 這遊戲開始時會隨機選一個字, 我們姑且稱這字為 "神祕字", 畫面中會有 ? 來代表這字內每一個字母, 例如這字有 11 個字母, 就會有這顯示:

  觀看者不知這是什麼字, 他要逐個字母來推測, 共有七次推測機會, 例如先估字母 A , 若這神祕字內有 A 的字母, 就會顯示出來, 如下:

  若再估字母 I, 這字母是神祕字內沒有的, 這就是一個錯誤, 就會有一幅圖片顯示被吊者的頭, 若有第二次錯誤, 就有被吊者的身驅出現, 再錯誤就有手及腳出現, 7 次錯誤這人就會被吊死, 而神祕字也會顯示出來:

圖 18-4 7 次錯誤這人就成形及被吊死

 

  請你用瀏覽器開啟示範磁碟中的 hang5.htm, 這是完整的遊戲, 也是這一節中完工後的程式, 請你先試試這遊戲的玩法, 筆者在這程式放了一百個中學程度的英文字, 請你在上窗格按你推測的字母, 一般會從 A、E、I、O、U這五個響音字母開始, 因為一個英文字必會有最少其中一個響音字母, 跟著推測 S、T、L 等較常用的字母。

  因為這程式較多資料, 所以筆者不逐步解釋, 請你自行研究這程式的原始檔案, 或買《網頁程式-JavaScript》這書看看吧, 書中對這程式的發展有詳細的解說。

 

  筆者在這處說的是 JavaScript 的操作, 你若真的將這類遊戲放在網上給人玩, 在圖片上應使用更活動的卡通漫畫, 觀看者成功估中一個字, 可用一個小視窗顯示一個跳躍的白免或肥豬等動畫, 這些動畫可以每次不同, 在小視窗的網頁開啟時, 並可伴以一小段鼓掌的聲音, 若失敗的就給他一個"喔 噢"的聲音。

  你也可將這遊戲作這設計: 字彙分為三個程度: 高級、中級、初級, 用三個文字陣列來記載, 程式開啟時, 觀看者選某個程度, 就是使用某個陣列的字彙, 這就可適應不同英文程度的觀看者。

 

 

 

18.5 "大/小"賭博遊戲

  在這一節, 筆者以一個大小賭博遊戲來作示範, 筆者不賭博, 也不想鼓勵賭博, 但一般來說, 賭博遊戲最能示範出程式的佈局邏輯, 讀者請用學習程式的眼光來看這處的 JavaScript。

  在前面說過程式的模組, 這處的遊戲也分為數個部份, 合併後就成為一個完整遊戲, 如下:

 

 請你用瀏覽器開啟示範磁碟中的 dice.htm, 這是完整的遊戲, 因為這程式較多資料, 所以筆者不逐步解釋, 請你自行研究這程式的原始檔案, 或買《網頁程式-JavaScript》這書看看吧, 書中對這程式的發展有詳細的解說。

  看完上述的檔案, 你可試造出更複雜的 21 點遊戲 (black jack), 或是較簡單的吃角子老虎機遊戲 (slot machine)。

 

(第18章完)