[初章] 03 反覆反覆再反覆

啊~測試好麻煩喔,每次輸入完都要關掉再重新執行。
可是,哪款遊戲game over之後就自動關掉,還要重新執行的?都會自動回標題畫面讓你再戰一次啊!
如果程式碼是有限的,總有一天會執行完,那麼它們是怎麼做到的?
答案,就在這裡!

流程控制中,除了條件式的執行,還有「有條件式的反覆執行」!舉個例子你就懂了:
不斷攻擊至敵人倒下為止 (條件: 敵人倒下;執行: 攻擊敵人)
熬夜背單字直至背完為止 (條件: 單字背完;執行: 背單字)
這章打完就去睡了 (條件: 這章打完;執行: 打這章。去睡是之後的事,之後再說)
這場戰爭後我就要回老家結婚了 (條件: 這場戰爭結束;執行: 戰爭。)
...


以上這些都是條件式反覆的簡單例子。甚至放寬來看,壽命結束之前每個日子也是一種條件式反覆。
隨便舉個例子,流程大概是像這樣:
1. 檢查敵人是否已倒下。已倒下時跳至步驟3.
2. 攻擊敵人,跳至步驟1.
3. 結束攻擊並休息

整個過程大概像這樣:
敵人是否倒下  --是--> 休息
    │
    否
    │
攻擊敵人
    │
    │
敵人是否倒下  --是--> 休息

    │
    否
    │
攻擊敵人
    │
    │
敵人是否倒下  --是--> 休息
……… (下略)

很簡單吧?一但條件滿足,就執行指令;執行完後再次檢查,條件依然滿足時就再次執行;執行完再次檢查,…循環下去
檢查、執行、檢查、執行、檢查、執行、檢查、執行、檢查、…
雖說是條件成立時反覆執行,但每次執行後都會重新檢查條件是否成立,並不是滿足條件就會永遠執行、不再重新檢查喔!

語法也很單純:
while(i<5)
{
    scanf("%d", &i);
}

語意是: 當…仍滿足的時候,執行…。
換句話說,就是: 直到…之前,不斷執行…。

讓我們分解語法看得更仔細:
while    <== 當...條件成立時,反覆執行...直到條件不成立
(    <== 條件放這對小括號裡面
i<5    <== 執行的條件,這裡只是個例子
)
{    <== 條件滿足時要執行的事,放在這對大括號裡面
scanf("%d", &i);    <== 條件滿足時執行的事,這裡只是個例子
}

語意上是: 當i<5仍滿足時,讓使用者輸入i。
換句話說,直到i<5不滿足之前,不斷讓使用者輸入i。
所以直到輸入i>=5之前,使用者都無法繼續,只會被要求重新輸入。

語意上了解了的話,廢話不多說,咱們就拿第一個例子: 不斷攻擊至敵人倒下為止!
先定義問題: 打怪!
大流程是: 一開始有一個敵人。如果敵人尚未倒下,就攻擊牠。反覆這個行為。
詳細一點的步驟如下:
  設定敵人初始狀態
  直到敵人倒下為止,進行以下動作:
    攻擊敵人

好,接下來我們把中文轉成C語言。
一開始必須要有敵人,且玩家可以攻擊牠。為了反映是否倒下,必須知道血量。
printf("please input hp of monster! ");
scanf("%d", &hp);    <== 讓玩家輸入血量
這樣我們得到了血量。

接著,直到敵人倒下為止,反覆以下動作,所以是while,條件是敵人尚未倒下。
while(hp > 0)    <== 血量>0表示還沒死
{
<== 裡面放攻擊的動作
}

攻擊要先知道傷害,由玩家輸入傷害好了。

printf("attack! what's the damage? ");
scanf("%d", &dmg);    <== 讓使用者輸入傷害多寡

接下來是真正做出傷害,並且為了方便觀察,回報接收的傷害與剩餘血量
hp-dmg;    <== 血量 減 造成傷害 等於 剩餘血量,完美的式子
printf("you give an attack with damage %d!\n", dmg);    <== 回報傷害
printf("the monster remains %d hp\n", hp);    <== 回報殘餘血量

然後我們把它拼起來。要先有血量才能判斷死活,先判斷死活才決定攻擊…

int hp, dmg;    <== 需要記憶血量和傷害
printf("please input hp of monster! ");
scanf("%d", &hp);    <== 先輸入怪物血量
while(hp > 0)    <== 當血量大於0,也就是還沒死!
{
    printf("attack! what's the damage? ");
    scanf("%d", &dmg);    <== 讓使用者輸入傷害多寡
    hp-dmg;    <== 血量 減 造成傷害 等於 剩餘血量,完美的式子
    printf("you give an attack with damage %d!\n", dmg);    <== 回報傷害
    printf("the monster remains %d hp\n", hp);    <== 回報殘餘血量
}
printf("the monster is dead! congratulations!\n");

好,讓我們來執行它!首先輸入100,然後送牠個召喚巴哈姆特9999去死吧啊哈哈哈哈
…咦?怎麼沒死!?這怎麼可能!而且竟然還是滿血!?明明就有9999傷害啊!死個99次都夠啊!怎麼回事!?

這說明了,hp-dmg根本沒動到記憶中hp的數值。即使我們認為意義很明顯,但在C語言解釋上這指令不會更動hp的值,就真的不會更動。
要知道對電腦而言,它根本不知道我們在做啥。每個操作的意義,都是我們自行強加上去的「解釋」。
我們沒辦法叫電腦打掃做飯,但是也許我們有辦法用電腦聽得懂的命令,讓他按我們所想的打掃做飯,就好像真的懂打掃做飯一樣。可是,電腦不知道他在做啥,對他來說只是一堆它能理解、能做到,卻不明白意義為何的指令。
將希望電腦做的事,清楚明白地轉成沒有任何暖昧模糊的指令步驟,再用C語言寫出來。寫程式就是這麼一回事。

所以我們必須學習記憶操作,以改變hp的值。
最基本、同時最萬用的操作,就是記憶的覆寫。
語法如下:
hp = 70;

執行這行指令,會將記憶中的hp覆蓋為70,原本記住的內容不會被保留。
看到懷念的=了!但上一篇講過:=並沒有「等於」的意義;兩個連在一起,==才有「等於」的意義喔!

一樣拆開來看:
hp    <== 想覆寫的記憶 (也就是說,變數) 是哪個
=    <== 把寫在它左邊的記憶的內容,替換成右邊的數字。符號的正式名稱為「賦值」。
70    <== 希望將記憶覆蓋成什麼樣的內容。必須「代表一個數字」。
;    <== 一個指令的結束

這有個毛用!?只能把hp以一個「在寫程式時就已決定好」的數字覆蓋掉罷了!
嘿嘿,當然不只啦!不然為何要把「代表一個數字」特別括起來?沒錯,有隱含的意義!
=右邊那一串,只要「代表一個數字」,放什麼都可以!比如說,2*7算不算代表一個數字?
算!代表的是計算結果14。也就是說,一個四則運算式也可以代表一個數字,即此運算式的運算結果!而且可以拿記憶中的內容來做運算!

攻擊造成dmg傷害,代表攻擊後hp會減少dmg這麼多。剩餘hp應該是hp-dmg!而我們希望的是,將原本的hp替換成hp-dmg!
hp = hp - dmg;    <== 操作目標: hp (左),新的內容: hp-dmg (右)

讓我們以中文試解釋之。這是一個指令,內容為: 計算出「此時」hp-dmg的值,並以此替換掉hp原先的值。
若計算時hp為100,dmg為70,計算結果為30,相當於hp = 30;所以hp的新內容就會是30了!簡直完美啊!

好,讓我們修改之前的程式碼,讓它更合理:

int hp, dmg;    <== 血量和傷害
printf("please input hp of monster! ");
scanf("%d", &hp);    <== 先輸入怪物血量
while(hp > 0)    <== 當血量大於0,也就是還沒死!
{
    printf("attack! what's the damage? ");
    scanf("%d", &dmg);    <== 讓使用者輸入傷害多寡
    hp = hp - dmg;    <== 將現有hp改為現有hp減去這次傷害(dmg)的數值
    printf("you give an attack with damage %d!\n", dmg);    <== 回報傷害
    printf("the monster remains %d hp\n", hp);    <== 回報殘餘血量
}
printf("the monster is dead! congratulations!\n");

試著玩玩看吧!這次來個龜派氣功砸它個9999就真的會陣亡啦!
接下來也要試試如果一擊不死,是否會繼續執行、殘餘hp計算是否正常。這樣才能夠確認你的程式有沒有錯。
這有個訣竅,就是: 你要站在評審或裁判之類的位置去想,想法子挑毛病或用合法但易錯的數據去想辦法搞爛它!如果把程式視為己出(事實上也沒錯啦),很容易就會忽略一些小地方沒去進行測試,就放過去了!

給你一個簡單的任務:
試著將上面程式碼,改寫為一篇以中文敘述的指令!
更進一步的任務:
將上面程式碼寫成一份規則書,請人來照著規則書扮演程式的角色,而你一樣扮演使用者,透過說話來傳遞輸入。扮演程式的人則透過說話來傳遞輸出!要求他不管合不合理,必須只按照規則書上寫的,一步一步操作,不可跳過、不可合併。

這樣的話,有沒有更了解程式是如何運作的呢?同樣,寫得夠詳細且定義完全不模糊的中文也可以翻成程式碼的喔!同樣都是語言嘛。
想練習的話,可以嘗試把上面的例題追加更多規則,像是: 追加防禦力的概念,每次被攻擊都會抵掉一定傷害;每次攻擊傷害不得為負數;每次攻擊超過1000就不作數,必須重新輸入;等等…

那麼最後,一樣來一道簡單的題目練習練習:
一開始有一隻hp為255的怪獸,而玩家的血量有100。每回合告訴玩家這是第幾回合,並給玩家輸入攻擊傷害,讓怪獸hp因受到攻擊而減少。怪獸受到攻擊後如果還活著,就會進行反擊,反擊傷害是10。
如果怪獸死亡了,就結束戰鬥,輸出恭喜的訊息並告知花費了幾回合。如果玩家死亡了,就輸出失敗打輸了的訊息。

以下是一個簡單的勝利範例過程,注意我所提到的資訊都必須確實告知玩家,不可遺漏:
monster hp: 255, you hp: 100   <== 需回報雙方hp,資訊要透明!
turn 1, attack!    <== 回合開始!記得要告知回合數喔!
what's the damage of this attack? 56    <== 56是玩家輸入的例子,當然不一定是56!
damage: 56, monster remain hp: 199    <== 要告知傷害及怪獸剩餘hp
monster counter!    <== 怪獸活著所以反擊了!
damage: 10, you remain hp: 90    <== 要告知傷害及玩家剩餘hp

turn 2, attack!    <== 回合數要確實變動
what's the damage of this attack? 112    <== 一樣由玩家自由輸入數字

damage: 112, monster remain hp: 87    <== 要告知傷害及怪獸剩餘hp
monster counter!    <== 怪獸活著所以反擊了!
damage: 10, you remain hp: 80    <== 要告知傷害及玩家剩餘hp

turn 3, attack!    <== 回合數要確實變動
what's the damage of this attack? 99
damage: 99, monster remain hp: -12
monster died!    <== 怪獸死了所以沒有反擊了
you defeat monster! spend 3 turns!    <== 告知勝利,並且告知花了多少回合。

以下是失敗範例過程:
monster hp: 255, you hp: 100
turn 1, attack!

…    (中略,假設過程總共只打了9的傷害…)

turn 10, attack!

what's the damage of this attack? 1
damage: 1, monster remain hp: 245
monster counter!
damage: 10, you remain hp: 0
you died!    <== 告知玩家,你死了
you are defeat! game over!    <== 失敗訊息,遊戲結束!


提示:
1. 先想好需要做的事有哪些、需要記憶的資訊有哪些,再來安排順序,安排完自己腦中跑過一遍覺得合理,再轉寫成程式碼
2. 善用&& (and 的意思,就是左右兩個條件同時成立,它才會成立)
   寫成 while( mon_hp, you_hp > 0 ) 是錯的。記住這是C語言,不是數學!
3. 自己慢慢想慢慢推敲,會比抄範例修改學到更多!
   學著不依賴抄程式碼,在你沒得抄時才有能力自行動手創造!
4. 試著將大件事拆成多件小事,分開考慮;最後再將小件事一步一步轉成程式的步驟。
   人類的思考是相當跳躍的,試著把事情的步驟拆得更細更蠢些!
5. 試著考慮每件事的特性,練習先用中文寫好規則,並翻譯成C語言。以下是參考一些。
   不只在當前指令,還需要保留到其它指令的資訊 (如: hp需要留到最後,檢查是否死亡;dmg需要留到輸入指令結束後,讓hp知道要扣多少;等等) 需要用變數來記憶它,否則指令結束就會遺忘
   當前狀態如果怎樣怎樣時該做什麼,就用if。例如怪獸還活著就反擊;死的是怪獸就是玩家勝利;等等
   在怎樣怎樣之前,還沒怎樣怎樣時,就用while。例如還沒分出勝負就繼續打;等等。
6. 在打鬥過程中,輸出的資訊越多越詳細,在出現意料外的錯誤時,能幫助你更容易分析問題所在!

沒有留言:

張貼留言