# 基于 Go 语言的 21 点游戏 RPC 交互系统 南京大学 计算机科学与技术学院 马一鸣 502024330038 ## 项目概述 本项目旨在使用 Go 语言设计并实现一款基于远程过程调用(RPC)机制进行通信的 Blackjack(即 21 点)游戏系统。系统由用户端和服务器端组成,用户可以通过用户端与服务器端的 AI 发牌员进行对战。整个系统的设计目标是提供流畅且公平的游戏体验,并通过可靠的通信机制保证用户与服务器之间的有效互动。 ## 需求分析 本项目的需求分析如下: 1. **通信机制:** 使用 RPC 作为用户和服务器之间的数据交互机制,以实现低延迟和高可靠性的远程通信。 2. **游戏规则:** 游戏采用 Blackjack 的基本规则,玩家与发牌员对战,以接近 21 点为目标。为了提高游戏性和互动性。 3. **策略设计:** 发牌员的策略可灵活设计,但必须遵循公平原则,保证游戏的可玩性。策略的制定应考虑不同场景下的最优选择,以增强游戏的挑战性。 4. **用户体验:** 系统应具备良好的用户交互设计,保证玩家在操作过程中的易用性和流畅性。 ## 概要设计 本项目基于 Go 语言实现 Blackjack 游戏系统,主要分为用户端和服务器端两部分。 - **用户端:** 用户端提供玩家操作界面,通过 RPC 机制向服务器端发送请求,进行拿牌、停牌等操作,并接收游戏结果。 - **服务器端:** 服务器端负责处理所有的游戏逻辑,包括初始化牌组、为玩家和发牌员发牌、计算点数、判断胜负等。服务器端通过 RPC 服务对外提供功能。 - **通信机制:** 使用 Go 的 `net/rpc` 库实现用户端与服务器端的通信,保证数据传输的安全性与实时性。 ## 详细设计 本项目的代码结构如下: ``` shell ❯ tree . . ├── client │ └── client.go ├── controller │ ├── response.go │ └── simulator.go ├── core │ └── game.go ├── doc.md ├── go.mod ├── main.go ├── model │ ├── card.go │ └── deck.go ├── rpc │ ├── request.go │ └── response.go ├── server │ └── server.go ├── util │ ├── dict.go │ └── status.go └── view └── viewer.go 9 directories, 15 files ``` 其中 21 点游戏的模拟部分遵循 `MVC` 设计模式。 * `model` 包负责对 21 点游戏中出现的各类对象(如卡牌 Card,卡组 Deck)等进行抽象。 * `view` 包负责将游戏的交互逻辑进行呈现。 * `controller` 包负责对于游戏的控制逻辑进行抽象,如发牌,要牌,判定胜负等交互逻辑。 * `game` 包负责将游戏整体进行封装,并实现了基于命令模式等指令解析模块。 对于 RPC 通信实现了如下包: * `rpc` 包封装了 RPC 通信所需要的结构体。 * `server` 包实现了 RPC 通信的服务端构建。 * `client` 包实现了 RPC 通信的客户端构建。 此外 `util` 包实现了各类轻量化工具代码。 ### 21 点游戏的模拟设计 #### 牌组设计 系统中的牌组包含 52 张牌,由四种花色(红桃、方块、梅花和黑桃)组成,每种花色有 13 张牌(2 到 10、J、Q、K、A)。每张牌的点数如下: - **数字牌 (2-10):** 牌的点数等于其面值。 - **人头牌 (J、Q、K):** 每张牌的点数为 10。 - **A 牌:** 可以视为 1 分或 11 分,具体取决于对玩家更有利的值。 在 `model/card.go` 中实现了对于 `Card` 结构体的封装: ```go type Card struct { Color string Value []int Name string Visible bool } ``` 在 `model/deck.go` 中实现了对于 `Deck` 结构体的封装: ```go type Deck struct { cards []*Card } ``` 一副卡组(Deck)由若干卡牌(Card)组成。与此同时,在 `Deck` 中封装了若干 API,最为重要的是抽牌 `Deck.Draw()` 与洗牌 `Deck.Shuffle()` API: ```go func (deck *Deck) Shuffle() *Deck { for i := range deck.cards { j := i + rand.Intn(len(deck.cards) - i) deck.cards[i], deck.cards[j] = deck.cards[j], deck.cards[i] } return deck } func (deck *Deck) Draw() *Card { if len(deck.cards) == 0 { return nil } card := deck.cards[0] deck.cards = deck.cards[1:] return card } ``` #### 游戏流程设计 在 `controller/simulator.go` 中封装了游戏的交互逻辑: ```go type BackJackSimulator struct { deck *model.Deck dealerCards *model.Deck playerCards *model.Deck dealerScore int playerScore int status util.Status } ``` 通过维护 `deck`,`dealerCards` 与 `playerCards` 三副卡组用来模拟 21 点的游戏流程。 设计实现游戏状态的初始化逻辑: ```go func (simulator *BackJackSimulator) Init() *BackJackSimulator { simulator.deck = model.NewDeck() simulator.dealerCards = model.NewDeck() simulator.playerCards = model.NewDeck() simulator.deck = initDeck(simulator.deck) simulator.dealerCards = initDealerCards(simulator.dealerCards, simulator.deck) simulator.playerCards = initPlayerCards(simulator.playerCards, simulator.deck) simulator.dealerScore, simulator.playerScore = 0, 0 simulator.status = util.INITIALIZED return simulator } ``` 值得注意的是,在 `BackJackSimulator` 中基于状态机模型对游戏的状态进行管理。 在游戏中共包含了四个不同的状态,分别是初始化(INITIALIZED),闲家胜利(WINNED),庄家胜利(FAILED)以及平局(DRAW),相关状态封装在 `util/status.go` 中: ```go type Status int const ( INITIALIZED Status = iota WINNED Status = iota FAILED Status = iota DRAW Status = iota ) ``` 游戏的流程如下: - **发牌阶段:** 游戏开始时,玩家和发牌员各自得到两张牌。玩家的两张牌均为明牌,发牌员有一张明牌和一张暗牌(底牌),例如发牌员的发牌逻辑如下: ```go if firstCard := deck.Draw(); deck != nil { dealerCards.Add(firstCard.Flip()) } if secondCard := deck.Draw(); deck != nil { dealerCards.Add(secondCard) } ``` - **要牌阶段:** 玩家可以选择“要牌”(Hit,即再抽一张牌)或“停牌”(Stand,保持当前手牌)。玩家可以多次选择“要牌”,直到总点数超过 21 点(称为“爆牌”)或选择“停牌”。 对于要牌逻辑与爆牌判定逻辑实现如下: ```go if card := simulator.deck.Draw(); card != nil { simulator.playerCards.Add(card) simulator.playerScore = 0 for _, card := range simulator.playerCards.Cards() { simulator.playerScore += card.Value[0] } if simulator.playerScore > 21 { simulator.status = util.FAILED return NewBlackJackResponse(400, "[🐧] Your total score exceeds 21 ! You lose !\n") } } ``` 值得注意的是在 `simulator` 中封装了 `BlackJackResponse` 结构体以返回游戏的状态反馈信息: ```go type BackJackResponse struct { Code int Description string } ``` 当玩家停止要牌之后即进入停牌阶段。 - **停牌阶段:** 发牌员揭开底牌后,必须继续“要牌”,直到手牌点数达到或超过 17 点: ```go for maxDealerScore < 17 { if card := simulator.deck.Draw(); card != nil { simulator.dealerCards.Add(card) if card.Name == "A" { maxDealerScore += card.Value[1] minDealerScore += card.Value[0] } else { maxDealerScore += card.Value[0] minDealerScore += card.Value[0] } } if minDealerScore > 21 { simulator.status = util.WINNED return NewBlackJackResponse(200, "[🐧] The dealer burst and you have won the game !\n") } } ``` - **胜负判定:** 游戏结束后,根据玩家和发牌员的点数判断胜负。若玩家爆牌则输掉游戏;若发牌员爆牌,则玩家获胜;若双方均未爆牌,则点数接近 21 的一方获胜: ```go if maxDealerScore > maxPlayerScore { simulator.status = util.FAILED return NewBlackJackResponse(200, "[🐧] The dealer wins the game !\n") } else if maxDealerScore < maxPlayerScore { simulator.status = util.WINNED return NewBlackJackResponse(200, "[🐧] You have won the game !\n") } else if maxDealerScore == maxPlayerScore { simulator.status = util.DRAW return NewBlackJackResponse(200, "[🐧] It is a draw !\n") } ``` #### 游戏界面设计 基于命令行对于游戏的界面进行设计。 在 `view/viewer.go` 中实现界面结构体: ```go type LiteralViewer struct { response string simulator *controller.BackJackSimulator } ``` 该结构体通过 `GetResponse` 结构输出游戏的状态信息。 将上述基于 MVC 架构的代码封装在 `BackJackGame` 结构体中: ```go type BackJackGame struct { simulator *controller.BackJackSimulator viewer *view.LiteralViewer } ``` 该结构通过命令模式对游戏逻辑进行控制: ```go func (game *BackJackGame) AddCommand(command string) string { command = strings.Split(command, " ")[0] response := "" switch command { case "start": {} case "hit": {} case "stand": {} case "help": {} default: {} } return response } ``` ### RPC 通信的客户端与服务器端设计 #### 服务端设计 服务器端的核心任务是处理游戏逻辑,并通过 RPC 服务向用户端提供接口。 服务端的核心代码如下: ```go commandService := new(CommandService) rpc.Register(commandService) listener, err := net.Listen("tcp", ":1234") if err != nil { fmt.Println("[🐧] Listening Failed:", err) return } defer listener.Close() fmt.Println("...") for { conn, err := listener.Accept() if err != nil { fmt.Println("[🐧] Connecting Failed:", err) continue } go rpc.ServeConn(conn) } ``` 上述代码实现了一个简单的 RPC 服务器,使用了 `Go` 语言中的 `net` 和 `rpc` 包。 在 `main` 函数中创建一个 `CommandService` 实例并将 `commandService` 实例注册到 RPC 服务器,使其公开方法可以被远程调用。 服务器开始监听 TCP 连接,端口号为 1234。`net.Listen()` 函数返回一个 listener(监听器)对象和一个错误(err)。 使用 `defer` 关键字确保在函数退出时关闭监听器,释放资源。 服务器等待客户端连接。当有客户端连接时,`listener.Accept()` 会返回一个表示该连接的 `conn` 对象。 通过 `go` 关键字保证每个连接都通过一个新的 `goroutine` 来处理,`rpc.ServeConn()` 用于处理该连接上的 RPC 请求。 对于 `CommandService` 结构体封装了调用方法如下: ```go func (cs *CommandService) ExecuteCommand(req brpc.Request, res *brpc.Response) error { command := req.Command response := backJackGame.AddCommand(command) res.Result = fmt.Sprint(response) return nil } ``` #### 客户端设计 客户端负责与玩家交互,并通过 RPC 向服务器发送请求。 客户端的核心代码如下: ```go client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { fmt.Println("[🐳] Connecting Failed:", err) return } defer client.Close() reader := bufio.NewReader(os.Stdin) for { command, _ := reader.ReadString('\n') command = command[:len(command)-1] if command == "exit" { break } req := brpc.Request{Command: command} var res brpc.Response err = client.Call("CommandService.ExecuteCommand", req, &res) if err != nil { fmt.Println("[🐳] RPC Failed:", err) continue } } ``` 上述代码使用 `rpc.Dial()` 函数创建一个 RPC 客户端,并通过 TCP 连接到 localhost 上的端口 1234。 使用 `defer` 关键字确保在函数结束时关闭 RPC 客户端,释放资源。 创建一个从标准输入读取用户输入的 `bufio.Reader`,用于从命令行接收用户输入。 通过 `client.Call()` 调用服务器端的 `CommandService.ExecuteCommand` 方法。 ## 测试与验证 欲运行服务端和客户端程序需要分别编译运行客户端与服务端代码: ```shell $ go run server.go ``` ```shell $ go run client.go ``` 其中客户端的交互页面如下: ```shell go run client.go [🐳] Input your Command (Input to quit): start ``` 在输入 `start` 命令后即可开启一局 21 点游戏: ```shell [🐳] Input your Command (Input to quit): start [🃏] Dealer: 🂠 ♥️10 [🎴] Player: ♦3 ♦7 [🐧] You have started a new Blackjack game. ``` 输入 `hit` 命令进行要牌操作: ```shell [🐳] Input your Command (Input to quit): hit [🃏] Dealer: 🂠 ♥️10 [🎴] Player: ♦3 ♦7 ♠A [🐧] You have drawn a card ! ``` 输入 `stand` 命令进行停牌操作: ```shell [🐳] Input your Command (Input to quit): stand [🃏] Dealer: ♠K ♥️10 [🎴] Player: ♦3 ♦7 ♠A [🐧] You decide to stand ! [🐧] You have won the game ! ``` 此时服务端也会输出对应的信息: ```shell ... [🐧] Command Received: start [🐧] Command Received: hit [🐧] Command Received: stand ``` 通过多轮测试与验证,本系统满足了 21 点游戏的交互逻辑的正确性。 # 结论 本项目通过使用 Go 语言和 RPC 通信机制,本项目实现了一款完整且可扩展的 21 点游戏系统。系统通过用户端与服务器端的分离,实现了逻辑处理和用户交互的独立性,从而提高了系统的扩展性和可维护性。项目通过详细的需求分析、概要设计和详细设计,确保了系统的功能完整性和用户体验。同时,通过系统测试,验证了系统的可靠性和稳定性。 未来可以考虑在以下方面进行改进: - 增加复杂的游戏规则,例如双倍下注、投降等,以提高游戏的策略性和娱乐性。 - 优化发牌员的策略,使其更具挑战性。 - 增加图形化用户界面,以提供更直观的游戏体验。 - 使用更加安全和高效的通信机制,以提高数据传输的安全性和系统性能。