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

回響