2024-11-01 00:46:26 +08:00

14 KiB
Raw Permalink Blame History

基于 Go 语言的 21 点游戏 RPC 交互系统

南京大学 计算机科学与技术学院 马一鸣 502024330038

项目概述

本项目旨在使用 Go 语言设计并实现一款基于远程过程调用RPC机制进行通信的 Blackjack即 21 点)游戏系统。系统由用户端和服务器端组成,用户可以通过用户端与服务器端的 AI 发牌员进行对战。整个系统的设计目标是提供流畅且公平的游戏体验,并通过可靠的通信机制保证用户与服务器之间的有效互动。

需求分析

本项目的需求分析如下:

  1. 通信机制: 使用 RPC 作为用户和服务器之间的数据交互机制,以实现低延迟和高可靠性的远程通信。
  2. 游戏规则: 游戏采用 Blackjack 的基本规则,玩家与发牌员对战,以接近 21 点为目标。为了提高游戏性和互动性。
  3. 策略设计: 发牌员的策略可灵活设计,但必须遵循公平原则,保证游戏的可玩性。策略的制定应考虑不同场景下的最优选择,以增强游戏的挑战性。
  4. 用户体验: 系统应具备良好的用户交互设计,保证玩家在操作过程中的易用性和流畅性。

概要设计

本项目基于 Go 语言实现 Blackjack 游戏系统,主要分为用户端和服务器端两部分。

  • 用户端: 用户端提供玩家操作界面,通过 RPC 机制向服务器端发送请求,进行拿牌、停牌等操作,并接收游戏结果。
  • 服务器端: 服务器端负责处理所有的游戏逻辑,包括初始化牌组、为玩家和发牌员发牌、计算点数、判断胜负等。服务器端通过 RPC 服务对外提供功能。
  • 通信机制: 使用 Go 的 net/rpc 库实现用户端与服务器端的通信,保证数据传输的安全性与实时性。

详细设计

本项目的代码结构如下:

 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 结构体的封装:

type Card struct {
	Color string
	Value []int
	Name string
	Visible bool
}

model/deck.go 中实现了对于 Deck 结构体的封装:

type Deck struct {
	cards []*Card
}

一副卡组Deck由若干卡牌Card组成。与此同时Deck 中封装了若干 API最为重要的是抽牌 Deck.Draw() 与洗牌 Deck.Shuffle() API

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 中封装了游戏的交互逻辑:

type BackJackSimulator struct {
	deck *model.Deck
	dealerCards *model.Deck
	playerCards *model.Deck
	dealerScore int
	playerScore int
	status util.Status
}

通过维护 deckdealerCardsplayerCards 三副卡组用来模拟 21 点的游戏流程。

设计实现游戏状态的初始化逻辑:

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 中:

type Status int

const (
	INITIALIZED Status = iota
	WINNED Status = iota
	FAILED Status = iota
	DRAW Status = iota
)

游戏的流程如下:

  • 发牌阶段: 游戏开始时,玩家和发牌员各自得到两张牌。玩家的两张牌均为明牌,发牌员有一张明牌和一张暗牌(底牌),例如发牌员的发牌逻辑如下:
	if firstCard := deck.Draw(); deck != nil {
		dealerCards.Add(firstCard.Flip())
	}
	if secondCard := deck.Draw(); deck != nil {
		dealerCards.Add(secondCard)
	}
  • 要牌阶段: 玩家可以选择“要牌”Hit即再抽一张牌或“停牌”Stand保持当前手牌。玩家可以多次选择“要牌”直到总点数超过 21 点(称为“爆牌”)或选择“停牌”。

对于要牌逻辑与爆牌判定逻辑实现如下:

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 结构体以返回游戏的状态反馈信息:

type BackJackResponse struct {
	Code int
	Description string
}

当玩家停止要牌之后即进入停牌阶段。

  • 停牌阶段: 发牌员揭开底牌后,必须继续“要牌”,直到手牌点数达到或超过 17 点:
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 的一方获胜:
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 中实现界面结构体:

type LiteralViewer struct {
	response string
	simulator *controller.BackJackSimulator
}

该结构体通过 GetResponse 结构输出游戏的状态信息。

将上述基于 MVC 架构的代码封装在 BackJackGame 结构体中:

type BackJackGame struct {
	simulator *controller.BackJackSimulator
	viewer *view.LiteralViewer
}

该结构通过命令模式对游戏逻辑进行控制:

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 服务向用户端提供接口。

服务端的核心代码如下:

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 语言中的 netrpc 包。

main 函数中创建一个 CommandService 实例并将 commandService 实例注册到 RPC 服务器,使其公开方法可以被远程调用。

服务器开始监听 TCP 连接,端口号为 1234。net.Listen() 函数返回一个 listener监听器对象和一个错误err

使用 defer 关键字确保在函数退出时关闭监听器,释放资源。

服务器等待客户端连接。当有客户端连接时,listener.Accept() 会返回一个表示该连接的 conn 对象。

通过 go 关键字保证每个连接都通过一个新的 goroutine 来处理,rpc.ServeConn() 用于处理该连接上的 RPC 请求。

对于 CommandService 结构体封装了调用方法如下:

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 向服务器发送请求。

客户端的核心代码如下:

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 方法。

测试与验证

欲运行服务端和客户端程序需要分别编译运行客户端与服务端代码:

$ go run server.go
$ go run client.go

其中客户端的交互页面如下:

go run client.go 
[🐳] Input your Command (Input <exit> to quit):
start    

在输入 start 命令后即可开启一局 21 点游戏:

[🐳] Input your Command (Input <exit> to quit):
start     
[🃏] Dealer: 🂠 ♥10 
[🎴] Player: ♦3 ♦7 
[🐧] You have started a new Blackjack game.

输入 hit 命令进行要牌操作:

[🐳] Input your Command (Input <exit> to quit):
hit
[🃏] Dealer: 🂠 ♥10 
[🎴] Player: ♦3 ♦7 ♠A 
[🐧] You have drawn a card !

输入 stand 命令进行停牌操作:

[🐳] Input your Command (Input <exit> to quit):
stand
[🃏] Dealer: ♠K ♥10 
[🎴] Player: ♦3 ♦7 ♠A 
[🐧] You decide to stand !
[🐧] You have won the game !

此时服务端也会输出对应的信息:

...
[🐧] Command Received: start
[🐧] Command Received: hit
[🐧] Command Received: stand

通过多轮测试与验证,本系统满足了 21 点游戏的交互逻辑的正确性。

结论

本项目通过使用 Go 语言和 RPC 通信机制,本项目实现了一款完整且可扩展的 21 点游戏系统。系统通过用户端与服务器端的分离,实现了逻辑处理和用户交互的独立性,从而提高了系统的扩展性和可维护性。项目通过详细的需求分析、概要设计和详细设计,确保了系统的功能完整性和用户体验。同时,通过系统测试,验证了系统的可靠性和稳定性。

未来可以考虑在以下方面进行改进:

  • 增加复杂的游戏规则,例如双倍下注、投降等,以提高游戏的策略性和娱乐性。
  • 优化发牌员的策略,使其更具挑战性。
  • 增加图形化用户界面,以提供更直观的游戏体验。
  • 使用更加安全和高效的通信机制,以提高数据传输的安全性和系统性能。