[Scala] 猜數字網路連線 RemoteActor 版。

昨天寫完猜數字的 AI,早上睡醒後突然想到,Scala 裡還有一個叫做 RemoteActor 的東西,雖然沒用過,但似乎是可以將 Actor 變成網路連線的版本,於是就試著寫寫看猜數字的網路連線版。

這個網路連線版的通訊協定及遊戲過程如下:

  1. 客戶端送一個整數代表學號給伺服器端
  2. 伺服器端收到學號後,會產生一個新的遊戲棋局(GuessNumberTurn),隨機挑一個不重覆的四位數當做正解
  3. 伺服器將此遊戲棋局的 ID 回覆給客戶端
  4. 收到棋局 ID 後客戶端就可以進行猜測,而每一次客戶端送給伺服器的猜測,都要帶著棋局 ID
  5. 伺服器端收到棋局 ID 和猜測後,將猜測和該棋局的正確解答比對,並將結果回給客戶端
  6. 當客戶端猜中正確解答時,棋局結束,清理資源,但伺服器必須繼續等待,看是否有其他人連線進來進行遊戲

當然,在整個解題的部份,和上一篇文章中所提到的邏輯是相同的,這一篇只是單純地將上一篇的例子改為網路連線的版本。

而這一個例子好玩的地方,在於你可以在下面的程式碼中發現,我們完全沒有定義我們的封包長什麼樣子,每一個 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

回響