2011-01-07 13:39 墳墓 (Brian Hsu)
昨天寫完猜數字的 AI,早上睡醒後突然想到,Scala 裡還有一個叫做 RemoteActor 的東西,雖然沒用過,但似乎是可以將 Actor 變成網路連線的版本,於是就試著寫寫看猜數字的網路連線版。
這個網路連線版的通訊協定及遊戲過程如下:
- 客戶端送一個整數代表學號給伺服器端
- 伺服器端收到學號後,會產生一個新的遊戲棋局(GuessNumberTurn),隨機挑一個不重覆的四位數當做正解
- 伺服器將此遊戲棋局的 ID 回覆給客戶端
- 收到棋局 ID 後客戶端就可以進行猜測,而每一次客戶端送給伺服器的猜測,都要帶著棋局 ID
- 伺服器端收到棋局 ID 和猜測後,將猜測和該棋局的正確解答比對,並將結果回給客戶端
- 當客戶端猜中正確解答時,棋局結束,清理資源,但伺服器必須繼續等待,看是否有其他人連線進來進行遊戲
當然,在整個解題的部份,和上一篇文章中所提到的邏輯是相同的,這一篇只是單純地將上一篇的例子改為網路連線的版本。
而這一個例子好玩的地方,在於你可以在下面的程式碼中發現,我們完全沒有定義我們的封包長什麼樣子,每一個 byte 是什麼東西,甚至感覺沒有任何處理到網路的部份--但他又確確實實是個網路程式!
神奇吧,這就是 Scala 讓人興奮的地方啊--你專注的是在解題,而不是瑣碎的細節。而如果你稍微把程式碼和上面的通訊協定對照一下,就會發現 Scala 真的很像『用嘴巴寫程式』。:p
廢話不多說,以下是客戶端的程式碼(我把上一篇講解過的註解刪掉了,讓程式碼比較清楚一點):
import scala.actors.Actor import scala.actors.Actor._ import scala.actors.remote.RemoteActor import scala.actors.remote.Node import scala.util.Random case class Status(x: String, a: Int, b: Int) case class Guess(gameID: Int, x: String) case class GameID(gameID: Int) case object SendOutStudentID trait GuessNumberFunctions { val generateAllPossible = { def hasRepeatNumber (x: String) = x.toSet.size != 4 def convertToPaddingString(x: Int) = "%04d" format (x) (0 to 9999). // 先產生 0 到 9999 的所有整數 map(convertToPaddingString). // 將這些整數全轉成補零成四位數的字串 filterNot(hasRepeatNumber) // 濾出所有不含重覆數字的部份 } def compare(number1: String, number2: String) = { def pairIsSame(x: (Char, Char)) = x._1 == x._2 val sumA = (number1 zip number2) filter (pairIsSame) val (remainNumber1, remainNumber2) = (number1 zip number2). filterNot (sumA contains _). unzip val sumB = remainNumber1 intersect remainNumber2 (sumA.length, sumB.length) } } class GuessNumberClient(host: String, port: Int, studentID: Int) extends Actor with GuessNumberFunctions { var gameID: Int = -1 var possibleAnswer = generateAllPossible def isSameStatus (number1: String, number2: String, nA: Int, nB: Int) = { compare(number1, number2) == (nA, nB) } def act () = loop { receive { case SendOutStudentID => println ("連線到 Server") val server = RemoteActor.select (Node("localhost", 9090), 'guessServer) println ("送出學號,準備開始遊戲") server ! studentID // 伺服器告知此局 ID case GameID(gameID) => println ("開始猜數字") this.gameID = gameID sender ! Guess(gameID, possibleAnswer.head) // 答對 4A case Status(myGuess, 4, 0) => println ("答案是:" + myGuess) Actor.exit() // 其他狀況的話用消去法求解 case Status(myGuess, a, b) => possibleAnswer = possibleAnswer.filter(isSameStatus(_, myGuess, a, b)) println ("現在的可能答案數:" + possibleAnswer.length) sender ! Guess(gameID, possibleAnswer.head) case otherwise => println ("Client 收到不明訊息:" + otherwise) } } } object GuessNumberClient { def main (args: Array[String]) { // 總共玩 10 次 for (i <- 0 until 10) { // 啟動客戶端 val client = new GuessNumberClient("localhost", 9090, 91213028) client.start() // 命令客戶端送學號給伺服器,告知開始遊戲 client ! SendOutStudentID // 等上一次的猜測結束 while (client.getState != scala.actors.Actor.State.Terminated) { Thread.sleep(100) } } } }
接著,是伺服器的程式碼:
import scala.actors.Actor import scala.actors.Actor._ import scala.actors.remote.RemoteActor import scala.actors.OutputChannel import scala.util.Random case class Status(x: String, a: Int, b: Int) case class Guess(gameID: Int, x: String) case class GameID(gameID: Int) case class FinishedGame(gameID: Int) trait GuessNumberFunctions { /** * 比對兩個猜數字字串後,回覆幾 A 幾 B * * 詳細的說明請參考[前一篇文章][1]的講解。 * * [1]: http://brianhsu.moe/blog/archives/1787 * * @param number1 第一個數字 * @param number2 第二個數字 */ def compare(number1: String, number2: String) = { def pairIsSame(x: (Char, Char)) = x._1 == x._2 val sumA = (number1 zip number2) filter (pairIsSame) val (remainNumber1, remainNumber2) = (number1 zip number2). filterNot (sumA contains _). unzip val sumB = remainNumber1 intersect remainNumber2 (sumA.length, sumB.length) } } class GuessNumberTurn(server: Actor, // 伺服器 Actor client: OutputChannel[Any], // 客戶端 Actor studentID: Int) extends Actor with GuessNumberFunctions { var count = 0 // 亂數選擇正確答案 val answer = Random.shuffle((0 to 9).toList) // 產生一個 List(0, 1, 2..., 9) 然後打亂之後 .take(4) // 取前四個數字,例:List(3,4,1,5) .mkString // 再結合成字串,例:"3415" // 印出正確答案 println ("[debug] answer:" + answer) // 持續 (loop) 地監聽事件 (react) def act () = loop { react { // 當進來的是一個猜數字的訊息(可以把他當做是封包之類的) case Guess(gameID, x) => // 比對客戶端的猜測和正確答案 val (statusA, statusB) = compare (answer, x) println ("%s => %dA%dB" format(x, statusA, statusB)) // 告知客戶端結果 client ! Status(x, statusA, statusB) // 計算此局猜測次數 count += 1 // 當 4A 時此次遊戲結束 if (statusA == 4) { println ("學號 %s 總共猜了 %d 次後答對" format(studentID, count)) // 告知伺服器將此棋局移除 server ! FinishedGame(gameID) // 結束此局 Actor.exit() } // 收到其他我不懂的訊息 case otherwise => println ("Server Helper Actor 收到不明訊息:" + otherwise) } } } class GuessNumberServer extends Actor { var gameTurns: Map[Int, GuessNumberTurn] = Map() def act () = { println ("建立 Server...") RemoteActor.alive (9090) RemoteActor.register ('guessServer, self) loop { react { // 如果收到學號 case studentID: Int => println ("StudentID:" + studentID) // 產生新局並記錄在表格中 val newTurn = new GuessNumberTurn(this, sender, studentID) gameTurns += (newTurn.hashCode -> newTurn) newTurn.start() // 告訴客戶端此局的 ID,代表遊戲開始 sender ! GameID(newTurn.hashCode) // 如果收到猜測 case Guess(gameID: Int, x: String) => // 傳遞給遊戲棋局 gameTurns(gameID) ! Guess(gameID, x) // 如果此局結束 case FinishedGame(gameID: Int) => // 將此局自表格中清除 println ("清理遊戲 ID:" + gameID) gameTurns -= gameID case otherwise => println ("Receive:" + otherwise) } } } } object GuessNumberServer { def main (args: Array[String]) { val server = new GuessNumberServer server.start() } }
完成這個程式以後的感想是:啥,就這樣?真的就這樣而已?!我該不會是在作夢吧……XD
回響